* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Uri; use InvalidArgumentException; use League\Uri\Exceptions\SyntaxError; use Nyholm\Psr7\Uri as NyholmUri; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Stringable; #[CoversClass(Uri::class)] #[Group('factory')] final class FactoryTest extends TestCase { #[DataProvider('invalidDataPath')] public function testCreateFromPathFailed(string $path): void { self::expectException(SyntaxError::class); Uri::fromFileContents($path); } public static function invalidDataPath(): array { return [ 'invalid format' => ['/usr/bin/yeah'], ]; } #[DataProvider('validFilePath')] public function testCreateFromPath(string $path, string $expected): void { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'header' => "Accept-language: en\r\nCookie: foo=bar\r\n", ], ]); $uri = Uri::fromFileContents(dirname(__DIR__).'/test_files/'.$path, $context); self::assertStringContainsString($expected, $uri->getPath()); } public static function validFilePath(): array { return [ 'text file' => ['hello-world.txt', 'text/plain'], 'img file' => ['red-nose.gif', 'image/gif'], ]; } #[DataProvider('provideValidData')] public function testFromData(string $data, string $mimetype, string $parameters, string $expected): void { self::assertSame($expected, Uri::fromData($data, $mimetype, $parameters)->toString()); } public static function provideValidData(): iterable { yield 'empty data' => [ 'data' => '', 'mimetype' => '', 'parameters' => '', 'expected' => 'data:text/plain;charset=us-ascii,', ]; yield 'simple string' => [ 'data' => 'Hello World!', 'mimetype' => '', 'parameters' => '', 'expected' => 'data:text/plain;charset=us-ascii,Hello%20World%21', ]; yield 'changing the mimetype' => [ 'data' => 'Hello World!', 'mimetype' => 'text/no-plain', 'parameters' => '', 'expected' => 'data:text/no-plain;charset=us-ascii,Hello%20World%21', ]; yield 'empty mimetype but added parameters' => [ 'data' => 'Hello World!', 'mimetype' => '', 'parameters' => 'foo=bar', 'expected' => 'data:text/plain;foo=bar,Hello%20World%21', ]; yield 'empty data with optional argument field' => [ 'data' => '', 'mimetype' => 'application/json', 'parameters' => 'foo=bar', 'expected' => 'data:application/json;foo=bar,', ]; yield 'changing the parameters' => [ 'data' => 'Hello World!', 'mimetype' => 'text/no-plain', 'parameters' => 'foo=bar', 'expected' => 'data:text/no-plain;foo=bar,Hello%20World%21', ]; yield 'the parameters can start with the ; character' => [ 'data' => 'Hello World!', 'mimetype' => 'text/no-plain', 'parameters' => ';foo=bar', 'expected' => 'data:text/no-plain;foo=bar,Hello%20World%21', ]; } public function testFromDataFailsWithInvalidMimeType(): void { $this->expectException(SyntaxError::class); Uri::fromData('Hello World!', 'text\plain'); } public function testFromDataFailsWithReservedParameterName(): void { $this->expectException(SyntaxError::class); Uri::fromData('Hello World!', 'text/plain', 'base64=foobar'); } public function testFromDataFailsWithMalformedParameters(): void { $this->expectException(SyntaxError::class); Uri::fromData('Hello World!', 'text/plain', 'foobar'); } #[DataProvider('unixpathProvider')] public function testCreateFromUnixPath(string $input, string $expected): void { self::assertSame($expected, Uri::fromUnixPath($input)->toString()); } public static function unixpathProvider(): array { return [ 'relative path' => [ 'input' => 'path', 'expected' => 'path', ], 'absolute path' => [ 'input' => '/path', 'expected' => 'file:///path', ], 'path with empty char' => [ 'input' => '/path empty/bar', 'expected' => 'file:///path%20empty/bar', ], 'relative path with dot segments' => [ 'input' => 'path/./relative', 'expected' => 'path/./relative', ], 'absolute path with dot segments' => [ 'input' => '/path/./../relative', 'expected' => 'file:///path/./../relative', ], ]; } #[DataProvider('windowLocalPathProvider')] public function testCreateFromWindowsLocalPath(string $input, string $expected): void { self::assertSame($expected, Uri::fromWindowsPath($input)->toString()); } public static function windowLocalPathProvider(): array { return [ 'relative path' => [ 'input' => 'path', 'expected' => 'path', ], 'relative path with dot segments' => [ 'input' => 'path\.\relative', 'expected' => 'path/./relative', ], 'absolute path' => [ 'input' => 'c:\windows\My Documents 100%20\foo.txt', 'expected' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt', ], 'windows relative path' => [ 'input' => 'c:My Documents 100%20\foo.txt', 'expected' => 'file:///c:My%20Documents%20100%2520/foo.txt', ], 'absolute path with `|`' => [ 'input' => 'c|\windows\My Documents 100%20\foo.txt', 'expected' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt', ], 'windows relative path with `|`' => [ 'input' => 'c:My Documents 100%20\foo.txt', 'expected' => 'file:///c:My%20Documents%20100%2520/foo.txt', ], 'absolute path with dot segments' => [ 'input' => '\path\.\..\relative', 'expected' => '/path/./../relative', ], 'absolute UNC path' => [ 'input' => '\\\\server\share\My Documents 100%20\foo.txt', 'expected' => 'file://server/share/My%20Documents%20100%2520/foo.txt', ], ]; } #[DataProvider('rfc8089UriProvider')] public function testCreateFromRfc8089(string $expected, string $uri): void { self::assertSame($expected, Uri::fromRfc8089($uri)->toString()); } public static function rfc8089UriProvider(): iterable { return [ 'empty authority' => [ 'expected' => 'file:///etc/fstab', 'uri' => 'file:/etc/fstab', ], 'localhost' => [ 'expected' => 'file:///etc/fstab', 'uri' => 'file://localhost/etc/fstab', ], 'file with authority' => [ 'expected' => 'file://yesman/etc/fstab', 'uri' => 'file://yesman/etc/fstab', ], ]; } #[TestWith(['invalidUri' => 'http://www.example.com'], 'unsupported scheme')] #[TestWith(['invalidUri' => '//localhost/etc/fstab'], 'missing scheme')] public function it_fails_to_generate_an_uri_from_rfc8089(string $invalidUri): void { $this->expectException(SyntaxError::class); Uri::fromRfc8089($invalidUri); } public function testCreateFromUri(): void { $expected = 'https://login:pass@secure.example.com:443/test/query.php?kingkong=toto#doc3'; $psr7 = Http::new($expected); $leagueUri = Uri::new($expected); $uriFromPsr7 = Uri::new($psr7); $uriFromLeagueUri = Uri::new($leagueUri); self::assertSame((string) $psr7, $uriFromPsr7->toString()); self::assertSame((string) $psr7, $uriFromLeagueUri->toString()); $uribis = Http::new(); self::assertSame((string) $uribis, Uri::new($uribis)->toString()); } #[DataProvider('validServerArray')] public function testCreateFromServer(string $expected, array $input): void { self::assertSame($expected, Uri::fromServer($input)->toString()); self::assertSame($expected, (string) Http::fromServer($input)); } public static function validServerArray(): array { return [ 'with host' => [ 'https://example.com:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => '23', 'HTTP_HOST' => 'example.com', ], ], 'server address IPv4' => [ 'https://127.0.0.1:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, ], ], 'server address IPv6' => [ 'https://[::1]:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '::1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, ], ], 'with port attached to host' => [ 'https://localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 80, 'HTTP_HOST' => 'localhost:23', ], ], 'with standard apache HTTP server' => [ 'http://localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => '', 'SERVER_PORT' => 80, 'HTTP_HOST' => 'localhost:23', ], ], 'with IIS HTTP server' => [ 'http://localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'off', 'SERVER_PORT' => 80, 'HTTP_HOST' => 'localhost:23', ], ], 'with IIS Rewritting server' => [ 'http://localhost:23/foo/bar?foo=bar', [ 'PHP_SELF' => '', 'IIS_WasUrlRewritten' => '1', 'UNENCODED_URL' => '/foo/bar?foo=bar', 'REQUEST_URI' => 'toto', 'SERVER_PORT' => 23, 'HTTP_HOST' => 'localhost', ], ], 'with standard port setting' => [ 'https://localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, 'HTTP_HOST' => 'localhost', ], ], 'without port' => [ 'https://localhost', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'HTTP_HOST' => 'localhost', ], ], 'with user info' => [ 'https://foo:bar@localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar', 'HTTPS' => 'on', 'SERVER_PORT' => 23, 'HTTP_HOST' => 'localhost:23', ], ], 'with user info and HTTP AUTHORIZATION' => [ 'https://foo:bar@localhost:23', [ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'SERVER_ADDR' => '127.0.0.1', 'HTTP_AUTHORIZATION' => 'basic '.base64_encode('foo:bar'), 'HTTPS' => 'on', 'SERVER_PORT' => 23, 'HTTP_HOST' => 'localhost:23', ], ], 'without request uri' => [ 'https://127.0.0.1:23/toto?foo=bar', [ 'PHP_SELF' => '/toto', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, 'QUERY_STRING' => 'foo=bar', ], ], 'without request uri and server host' => [ 'https://127.0.0.1:23', [ 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, ], ], ]; } public function testFailCreateFromServerWithoutHost(): void { self::expectException(SyntaxError::class); Uri::fromServer([ 'PHP_SELF' => '', 'REQUEST_URI' => '', 'HTTPS' => 'on', 'SERVER_PORT' => 23, ]); } public function testFailCreateFromServerWithoutInvalidUserInfo(): void { self::expectException(SyntaxError::class); Uri::fromServer([ 'PHP_SELF' => '/toto', 'SERVER_ADDR' => '127.0.0.1', 'HTTPS' => 'on', 'SERVER_PORT' => 23, 'QUERY_STRING' => 'foo=bar', 'HTTP_AUTHORIZATION' => 'basic foo:bar', ]); } #[DataProvider('createProvider')] public function testCreateFromBaseUri(Stringable|string $baseUri, Stringable|string $uri, string $expected): void { $newUri = Uri::parse($uri, $baseUri); self::assertInstanceOf(Uri::class, $newUri); self::assertSame($expected, $newUri->toString()); } public static function createProvider(): array { $base_uri = 'http://a/b/c/d;p?q'; return [ 'base uri' => [$base_uri, '', $base_uri], 'scheme' => [$base_uri, 'http://d/e/f', 'http://d/e/f'], 'path 1' => [$base_uri, 'g', 'http://a/b/c/g'], 'path 2' => [$base_uri, './g', 'http://a/b/c/g'], 'path 3' => [$base_uri, 'g/', 'http://a/b/c/g/'], 'path 4' => [$base_uri, '/g', 'http://a/g'], 'authority' => [$base_uri, '//g', 'http://g'], 'query' => [$base_uri, '?y', 'http://a/b/c/d;p?y'], 'path + query' => [$base_uri, 'g?y', 'http://a/b/c/g?y'], 'fragment' => [$base_uri, '#s', 'http://a/b/c/d;p?q#s'], 'path + fragment' => [$base_uri, 'g#s', 'http://a/b/c/g#s'], 'path + query + fragment' => [$base_uri, 'g?y#s', 'http://a/b/c/g?y#s'], 'single dot 1' => [$base_uri, '.', 'http://a/b/c/'], 'single dot 2' => [$base_uri, './', 'http://a/b/c/'], 'single dot 3' => [$base_uri, './g/.', 'http://a/b/c/g/'], 'single dot 4' => [$base_uri, 'g/./h', 'http://a/b/c/g/h'], 'double dot 1' => [$base_uri, '..', 'http://a/b/'], 'double dot 2' => [$base_uri, '../', 'http://a/b/'], 'double dot 3' => [$base_uri, '../g', 'http://a/b/g'], 'double dot 4' => [$base_uri, '../..', 'http://a/'], 'double dot 5' => [$base_uri, '../../', 'http://a/'], 'double dot 6' => [$base_uri, '../../g', 'http://a/g'], 'double dot 7' => [$base_uri, '../../../g', 'http://a/g'], 'double dot 8' => [$base_uri, '../../../../g', 'http://a/g'], 'double dot 9' => [$base_uri, 'g/../h' , 'http://a/b/c/h'], 'mulitple slashes' => [$base_uri, 'foo////g', 'http://a/b/c/foo////g'], 'complex path 1' => [$base_uri, ';x', 'http://a/b/c/;x'], 'complex path 2' => [$base_uri, 'g;x', 'http://a/b/c/g;x'], 'complex path 3' => [$base_uri, 'g;x?y#s', 'http://a/b/c/g;x?y#s'], 'complex path 4' => [$base_uri, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'], 'complex path 5' => [$base_uri, 'g;x=1/../y', 'http://a/b/c/y'], 'dot segments presence 1' => [$base_uri, '/./g', 'http://a/g'], 'dot segments presence 2' => [$base_uri, '/../g', 'http://a/g'], 'dot segments presence 3' => [$base_uri, 'g.', 'http://a/b/c/g.'], 'dot segments presence 4' => [$base_uri, '.g', 'http://a/b/c/.g'], 'dot segments presence 5' => [$base_uri, 'g..', 'http://a/b/c/g..'], 'dot segments presence 6' => [$base_uri, '..g', 'http://a/b/c/..g'], 'origin uri without path' => ['http://h:b@a', 'b/../y', 'http://h:b@a/y'], 'uri without auhtority' => ['mailto:f@a.b', 'b@c.d?subject=baz', 'mailto:b@c.d?subject=baz'], 'with PSR7 base URI' => [new NyholmUri($base_uri), 'g.', 'http://a/b/c/g.'], 'with PSR7 ref URI' => [$base_uri, new NyholmUri('g.'), 'http://a/b/c/g.'], 'with PSR7 base URI and ref URI' => [new NyholmUri($base_uri), new NyholmUri('g.'), 'http://a/b/c/g.'], ]; } public function testCreateThrowExceptionWithBaseUriNotAbsolute(): void { self::assertNull(Uri::parse('/path/to/you', '//example.com')); } #[TestWith(['data:text/plain;charset=us-ascii,'])] #[TestWith(['/path/to/you'])] public function testCreateWithUriWithoutAuthority(string $expected): void { $uri = Uri::parse($expected); self::assertInstanceOf(Uri::class, $uri); self::assertSame($expected, $uri->toString()); } public function testCreateWithAbsoluteUriWithoutBaseUri(): void { $expected = 'scheme://host/sky?q#f'; $uri = Uri::parse('scheme://host/path/../sky?q#f'); self::assertInstanceOf(Uri::class, $uri); self::assertNotSame($expected, $uri->toString()); self::assertSame($expected, $uri->normalize()->toString()); } public function testCreateFromComponentsWithNullPath(): void { self::assertSame('', Uri::fromComponents(['path' => null])->toString()); } public function testICanBeInstantiateFromRFC6750(): void { $template = 'https://example.com/hotels/{hotel}/bookings/{booking}'; $params = ['booking' => '42', 'hotel' => 'Rest & Relax']; self::assertSame( '/hotels/Rest%20%26%20Relax/bookings/42', Uri::fromTemplate($template, $params)->getPath() ); } #[Test] #[TestWith(['uri' => ' '], 'uri containing only whitespaces')] #[TestWith(['uri' => ' https://a/b?c'], 'uri starting with whitespaces')] #[TestWith(['uri' => 'https://a/b?c '], 'uri ending with whitespaces')] public function it_fails_parsing_uri_string_with_whitespace(string $uri): void { $this->expectException(InvalidArgumentException::class); Uri::new($uri); } #[Test] #[DataProvider('provideInvalidUri')] public function it_fails_to_parse_with_new(Stringable|string $uri): void { self::assertNull(Uri::tryNew($uri)); } public static function provideInvalidUri(): iterable { yield 'invalid URI' => ['uri' => 'http://example.com/ ']; } #[Test] #[TestWith(['uri' => ':', 'baseUri' => null], 'invalid URI')] #[TestWith(['uri' => '', 'baseUri' => '/absolute/path'], 'invalid resolution with a non absolute URI')] public function it_fails_to_parse_with_parse(Stringable|string $uri, Stringable|string|null $baseUri): void { self::assertNull(Uri::parse($uri, $baseUri)); } #[Test] public function it_can_create_an_new_instance_with_space_using_from_base_uri_bugfix175(): void { self::assertSame( 'http://www.example.com/path%20with%20space/', Uri::fromBaseUri('http://www.example.com/path with space/')->toString() ); self::assertSame( 'http://www.example.com/path%20with%20space/', Uri::fromBaseUri('/path with space/', 'http://www.example.com')->toString() ); } }