diff --git a/composer.json b/composer.json index a639d51ca..bfd95859c 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "symfony/polyfill-php82": "self.version", "symfony/polyfill-php83": "self.version", "symfony/polyfill-php84": "self.version", + "symfony/polyfill-php85": "self.version", "symfony/polyfill-iconv": "self.version", "symfony/polyfill-intl-grapheme": "self.version", "symfony/polyfill-intl-icu": "self.version", @@ -62,6 +63,7 @@ "src/Intl/Icu/Resources/stubs", "src/Intl/MessageFormatter/Resources/stubs", "src/Intl/Normalizer/Resources/stubs", + "src/Php85/Resources/stubs", "src/Php84/Resources/stubs", "src/Php83/Resources/stubs", "src/Php82/Resources/stubs", diff --git a/src/Php85/Exception/ParsingException.php b/src/Php85/Exception/ParsingException.php new file mode 100644 index 000000000..fa91e6054 --- /dev/null +++ b/src/Php85/Exception/ParsingException.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Uri; + +if (\PHP_VERSION_ID < 80500) { + class Rfc3986Uri extends \Symfony\Polyfill\Php85\Uri\Rfc3986Uri + { + } +} diff --git a/src/Php85/Resources/stubs/Uri/Uri.php b/src/Php85/Resources/stubs/Uri/Uri.php new file mode 100644 index 000000000..3af7b6307 --- /dev/null +++ b/src/Php85/Resources/stubs/Uri/Uri.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Uri; + +use Symfony\Polyfill\Php85 as p; + +if (\PHP_VERSION_ID < 80500) { + abstract class Uri extends p\Uri\Uri + { + } +} diff --git a/src/Php85/Resources/stubs/Uri/WhatWgError.php b/src/Php85/Resources/stubs/Uri/WhatWgError.php new file mode 100644 index 000000000..0f6240cff --- /dev/null +++ b/src/Php85/Resources/stubs/Uri/WhatWgError.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Uri; + +if (\PHP_VERSION_ID < 80500) { + final class WhatWgError extends \Symfony\Polyfill\Php85\Uri\WhatWgError + { + } +} diff --git a/src/Php85/Resources/stubs/Uri/WhatWgUri.php b/src/Php85/Resources/stubs/Uri/WhatWgUri.php new file mode 100644 index 000000000..1a8c38096 --- /dev/null +++ b/src/Php85/Resources/stubs/Uri/WhatWgUri.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Uri; + +if (\PHP_VERSION_ID < 80500) { + class WhatWgUri extends \Symfony\Polyfill\Php85\Uri\WhatWgUri + { + } +} diff --git a/src/Php85/Uri/Rfc3986Uri.php b/src/Php85/Uri/Rfc3986Uri.php new file mode 100644 index 000000000..cecd6c301 --- /dev/null +++ b/src/Php85/Uri/Rfc3986Uri.php @@ -0,0 +1,121 @@ + + * + * @internal + */ +class Rfc3986Uri extends \Uri\Uri +{ + public function __construct(string $uri, ?string $baseUrl = null) + { + if ('' === trim($uri)) { + throw new \ValueError('Argument #1 ($uri) cannot be empty'); + } + + if (null !== $baseUrl && '' === trim($baseUrl)) { + throw new \ValueError('Argument #2 ($baseUrl) cannot be empty'); + } + + try { + $this->parse($uri, $baseUrl); + } catch (ParsingException $exception) { + throw new \Error('Argument #1 ($uri) must be a valid URI'); + } + + $this->initialized = true; + } + + private function parse(string $uri, ?string $baseUrl): void + { + if (!preg_match('/^[a-zA-Z][a-zA-Z\d+\-.]*:/', $uri) && null !== $baseUrl) { + // uri is a relative uri and bse url exists + $this->parse(rtrim($baseUrl, '/').'/'.ltrim($uri, '/'), null); + + return; + } + + if (preg_match('/[^\x20-\x7e]/', $uri)) { + // the string contains non-ascii chars + throw new ParsingException(); + } + + preg_match(self::URI_GLOBAL_REGEX, $uri, $matches); + if (!$matches || !isset($matches['scheme']) || '' === $matches['scheme']) { + //throw new InvalidUriException($uri); + } + + if (preg_match('~'.$matches['scheme'].':/(?!/)~', $uri)) { + //throw new InvalidUriException($uri); + } + + if (isset($matches['authority'])) { + if (!str_contains($uri, '://') && '' !== $matches['authority']) { + //throw new InvalidUriException($uri); + } + + preg_match(self::URI_AUTHORITY_REGEX, $matches['authority'], $authMatches); + + $matches = array_merge($matches, $authMatches); + unset($matches['authority']); + } + + $matches = array_filter($matches, function (string $value) { return '' !== $value; }); + + if (isset($matches['host']) && false === \filter_var($matches['host'], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + // the host contains invalid code points + throw new ParsingException(); + } + + $this->scheme = $matches['scheme'] ?? null; + $this->user = isset($matches['user']) ? rawurldecode($matches['user']) : null; + $this->password = isset($matches['pass']) ? rawurldecode($matches['pass']) : null; + $this->host = $matches['host'] ?? null; + $this->port = $matches['port'] ?? null; + $this->path = isset($matches['path']) ? ltrim($matches['path'], '/') : null; + $this->query = $matches['query'] ?? null; + $this->fragment = $matches['fragment'] ?? null; + } + + public function __toString() + { + $uri = ''; + + if (null !== $this->scheme) { + $uri .= $this->scheme.':'; + } + + if (null !== $this->host) { + $uri .= '//'; + if (null !== $this->user) { + $uri .= rawurlencode($this->user); + if (null !== $this->password) { + $uri .= ':'.rawurlencode($this->password); + } + $uri .= '@'; + } + $uri .= $this->host; + if (null !== $this->port) { + $uri .= ':'.$this->port; + } + } + + if (null !== $this->path) { + $uri .= '/'.$this->path; + } + + if (null !== $this->query) { + $uri .= '?'.$this->query; + } + + if (null !== $this->fragment) { + $uri .= '#'.$this->fragment; + } + + return $uri; + } +} diff --git a/src/Php85/Uri/Uri.php b/src/Php85/Uri/Uri.php new file mode 100644 index 000000000..c8f8e64ef --- /dev/null +++ b/src/Php85/Uri/Uri.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php85\Uri; + +/** + * @author Alexandre Daubois + * + * @internal + */ +abstract class Uri implements \Stringable +{ + protected const URI_GLOBAL_REGEX = '/^(?:(?P[^:\/?#]+):)?(?:\/\/(?P[^\/?#]*))?(?P[^?#]*)(?:\?(?P[^#]*))?(?:#(?P.*))?$/'; + protected const URI_AUTHORITY_REGEX = '/^(?:(?P[^:@]*)(?::(?P[^@]*))?@)?(?P[^:]*)(?::(?P\d*))?$/'; + protected $initialized = false; + + /** + * @var string|null + */ + protected $scheme; + + /** + * @var string|null + */ + protected $user; + + /** + * @var string|null + */ + protected $password; + + /** + * @var string|null + */ + protected $host; + + /** + * @var int|null + */ + protected $port; + + /** + * @var string|null + */ + protected $path; + + /** + * @var string|null + */ + protected $query; + + /** + * @var string|null + */ + protected $fragment; + + public static function fromRfc3986(string $uri, ?string $baseUrl = null): ?static + { + try { + return new Rfc3986Uri($uri, $baseUrl); + } catch (\ValueError $error) { + throw $error; + } catch (\Error $error) { + return null; + } + } + + /** + * @param array $errors + */ + public static function fromWhatWg(string $uri, ?string $baseUrl = null, &$errors = null): ?static + { + $uri = new WhatWgUri($uri, $baseUrl, $errors); + + if ($errors) { + return null; + } + + return $uri; + } + + public function getScheme(): ?string + { + $this->ensureInitialized(); + + return $this->scheme; + } + + public function getUser(): ?string + { + $this->ensureInitialized(); + + return $this->user; + } + + public function getPassword(): ?string + { + $this->ensureInitialized(); + + return $this->password; + } + + public function getHost(): ?string + { + $this->ensureInitialized(); + + return $this->host; + } + + public function getPort(): ?int + { + $this->ensureInitialized(); + + return $this->port; + } + + public function getPath(): ?string + { + $this->ensureInitialized(); + + return $this->path; + } + + public function getQuery(): ?string + { + $this->ensureInitialized(); + + return $this->query; + } + + public function getFragment(): ?string + { + $this->ensureInitialized(); + + return $this->fragment; + } + + private function ensureInitialized(): void + { + if (!$this->initialized) { + throw new \Error(\sprintf('%s object is not correctly initialized', static::class)); + } + } +} diff --git a/src/Php85/Uri/WhatWgError.php b/src/Php85/Uri/WhatWgError.php new file mode 100644 index 000000000..4627ef20d --- /dev/null +++ b/src/Php85/Uri/WhatWgError.php @@ -0,0 +1,57 @@ + + * + * @internal + */ +class WhatWgError +{ + public const ERROR_TYPE_DOMAIN_TO_ASCII = 0; + public const ERROR_TYPE_DOMAIN_TO_UNICODE = 1; + public const ERROR_TYPE_DOMAIN_INVALID_CODE_POINT = 2; + public const ERROR_TYPE_HOST_INVALID_CODE_POINT = 3; + public const ERROR_TYPE_IPV4_EMPTY_PART = 4; + public const ERROR_TYPE_IPV4_TOO_MANY_PARTS = 5; + public const ERROR_TYPE_IPV4_NON_NUMERIC_PART = 6; + public const ERROR_TYPE_IPV4_NON_DECIMAL_PART = 7; + public const ERROR_TYPE_IPV4_OUT_OF_RANGE_PART = 8; + public const ERROR_TYPE_IPV6_UNCLOSED = 9; + public const ERROR_TYPE_IPV6_INVALID_COMPRESSION = 10; + public const ERROR_TYPE_IPV6_TOO_MANY_PIECES = 11; + public const ERROR_TYPE_IPV6_MULTIPLE_COMPRESSION = 12; + public const ERROR_TYPE_IPV6_INVALID_CODE_POINT = 13; + public const ERROR_TYPE_IPV6_TOO_FEW_PIECES = 14; + public const ERROR_TYPE_IPV4_IN_IPV6_TOO_MANY_PIECES = 15; + public const ERROR_TYPE_IPV4_IN_IPV6_INVALID_CODE_POINT = 16; + public const ERROR_TYPE_IPV4_IN_IPV6_OUT_OF_RANGE_PART = 17; + public const ERROR_TYPE_IPV4_IN_IPV6_TOO_FEW_PARTS = 18; + public const ERROR_TYPE_INVALID_URL_UNIT = 19; + public const ERROR_TYPE_SPECIAL_SCHEME_MISSING_FOLLOWING_SOLIDUS = 20; + public const ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL = 21; + public const ERROR_TYPE_INVALID_REVERSE_SOLIDUS = 22; + public const ERROR_TYPE_INVALID_CREDENTIALS = 23; + public const ERROR_TYPE_HOST_MISSING = 24; + public const ERROR_TYPE_PORT_OUT_OF_RANGE = 25; + public const ERROR_TYPE_PORT_INVALID = 26; + public const ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER = 27; + public const ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER_HOST = 28; + + /** + * @var string + */ + public $position; + + /** + * @var int + */ + public $errorCode; + + public function __construct(string $position, int $errorCode) + { + $this->position = $position; + $this->errorCode = $errorCode; + } +} \ No newline at end of file diff --git a/src/Php85/Uri/WhatWgUri.php b/src/Php85/Uri/WhatWgUri.php new file mode 100644 index 000000000..c7472d964 --- /dev/null +++ b/src/Php85/Uri/WhatWgUri.php @@ -0,0 +1,190 @@ + + * + * @internal + */ +class WhatWgUri extends \Uri\Uri +{ + /** + * @param array $errors + */ + public function __construct(string $uri, ?string $baseUrl = null, &$errors = null) + { + if ('' === trim($uri)) { + throw new \ValueError('Argument #1 ($uri) cannot be empty'); + } + + if (null !== $baseUrl && '' === trim($baseUrl)) { + throw new \ValueError('Argument #2 ($baseUrl) cannot be empty'); + } + + $this->parse($uri, $baseUrl, $errors); + + $this->initialized = true; + } + + public function __toString() + { + $uri = ''; + + if (null !== $this->scheme) { + $uri .= $this->scheme.':'; + } + + if (null !== $this->host) { + $uri .= '//'; + if (null !== $this->user) { + $uri .= rawurlencode($this->user); + if (null !== $this->password) { + $uri .= ':'.rawurlencode($this->password); + } + $uri .= '@'; + } + $uri .= idn_to_utf8($this->host); + if (null !== $this->port) { + $uri .= ':'.$this->port; + } + } + + $uri .= '/'; + if (null !== $this->path) { + $uri .= $this->path; + } + + if (null !== $this->query) { + $uri .= '?'.$this->query; + } + + if (null !== $this->fragment) { + $uri .= '#'.$this->fragment; + } + + return $uri; + } + + private function parse(string $uri, ?string $baseUrl, &$errors = null): void + { + if (!preg_match('/^[a-zA-Z][a-zA-Z\d+\-.]*:/', $uri) && null !== $baseUrl) { + // uri is a relative uri and bse url exists + $this->parse(rtrim($baseUrl, '/').'/'.ltrim($uri, '/'), null); + + return; + } + + preg_match(self::URI_GLOBAL_REGEX, $uri, $matches); + if (!$matches || !isset($matches['scheme']) || '' === $matches['scheme']) { + //throw new InvalidUriException($uri); + $errors[] = new WhatWgError($uri, WhatWgError::ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL); + + return; + } + + if (preg_match('~'.$matches['scheme'].':/(?!/)~', $uri)) { + $errors[] = new WhatWgError($uri, WhatWgError::ERROR_TYPE_SPECIAL_SCHEME_MISSING_FOLLOWING_SOLIDUS); + + return; + } + + if (isset($matches['authority'])) { + if (!str_contains($uri, '://') && '' !== $matches['authority']) { + //throw new InvalidUriException($uri); + return; + } + + preg_match(self::URI_AUTHORITY_REGEX, $matches['authority'], $authMatches); + + $matches = array_merge($matches, $authMatches); + unset($matches['authority']); + } + + $matches = array_filter($matches, function (string $value) { return '' !== $value; }); + + $host = null; + if (isset($matches['host'])) { + $host = $this->handleHost($matches['host'], $errors); + } + + $path = null; + if (isset($matches['path'])) { + $path = $this->resolvePath($this->handlePath(ltrim($matches['path'], '/'))); + } + + $this->scheme = isset($matches['scheme']) ? strtolower($matches['scheme']) : null; + $this->user = isset($matches['user']) ? rawurldecode($matches['user']) : null; + $this->password = isset($matches['pass']) ? rawurldecode($matches['pass']) : null; + $this->host = $host; + $this->port = $matches['port'] ?? null; + $this->path = $path; + $this->query = isset($matches['query']) ? $this->encodeQueryParams($matches['query']) : null; + $this->fragment = $matches['fragment'] ?? null; + } + + private function handleHost(string $host, &$errors): string + { + $host = strtolower($host); + + // validate hostname and ensure there isn't any invalid code point + if ($host === idn_to_ascii($host) && false === filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + $errors[] = new WhatWgError($host, WhatWgError::ERROR_TYPE_DOMAIN_INVALID_CODE_POINT); + } + + // check if host is an IP address using octal notation + if (preg_match('/^(0[0-7]+\.){3}(0[0-7]+)$/', $host)) { + // decode each part, remove leading zeros and re-encode in reverse order + $host = \explode('.', $host); + $host = array_map(function ($part) { + $part = ltrim($part, '0'); + + return (string) octdec($part); + }, array_reverse($host)); + + $host = implode('.', $host); + } + + return idn_to_ascii($host); + } + + private function resolvePath(string $path): string + { + $relativeParts = explode('/', $path); + $resolvedPathSegments = []; + + foreach ($relativeParts as $segment) { + if ('..' === $segment) { + array_pop($resolvedPathSegments); + } elseif ('.' !== $segment && '' !== $segment) { + $resolvedPathSegments[] = $segment; + } + } + + return implode('/', $resolvedPathSegments); + } + + private function handlePath(string $path): string + { + // check if the path looks like a Windows path + if (preg_match('/^(?P[a-zA-Z]):[\\/\\\\](?P.*)/', $path, $matches)) { + $filePath = ltrim(str_replace('\\', '/', $matches['path']), '/'); + $path = $matches['drive'].':/'.rawurlencode($filePath); + } + + return $path; + } + + private function encodeQueryParams(string $query): string + { + $queryParts = explode('&', $query); + $encodedQueryParts = []; + + foreach ($queryParts as $queryPart) { + $queryPartParts = explode('=', $queryPart, 2); + $encodedQueryParts[] = rawurlencode($queryPartParts[0]).(isset($queryPartParts[1]) ? '='.urlencode(urldecode($queryPartParts[1])) : ''); + } + + return implode('&', $encodedQueryParts); + } +} \ No newline at end of file diff --git a/src/Php85/composer.json b/src/Php85/composer.json new file mode 100644 index 000000000..590d58c6e --- /dev/null +++ b/src/Php85/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/polyfill-php85", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Alexandre Daubois", + "email": "alex.daubois@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2", + "symfony/polyfill-php80": "^1" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php85\\": "" }, + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/tests/Php85/Rfc3986UriTest.php b/tests/Php85/Rfc3986UriTest.php new file mode 100644 index 000000000..59263bf14 --- /dev/null +++ b/tests/Php85/Rfc3986UriTest.php @@ -0,0 +1,159 @@ +assertSame('https', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + } + + public function testParseUrlWithElements() + { + $uri = Uri::fromRfc3986('https://username:password@example.com:8080/pathname1/pathname2/pathname3?query=true#hash-exists'); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('pathname1/pathname2/pathname3', $uri->getPath()); + $this->assertSame('query=true', $uri->getQuery()); + $this->assertSame('hash-exists', $uri->getFragment()); + } + + public function testParseExoticUrl() + { + $this->assertNull(Uri::fromRfc3986('http://username:password@héééostname:9090/gah/../path?arg=vaéue#anchor')); + } + + public function testParsePath() + { + $uri = Uri::fromRfc3986('/page:1'); + + $this->assertNull($uri->getScheme()); + $this->assertNull($uri->getHost()); + $this->assertNull($uri->getPort()); + $this->assertSame('page:1', $uri->getPath()); + } + + public function testEmptyUrl() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Argument #1 ($uri) cannot be empty'); + + Uri::fromRfc3986(''); + } + + public function testEmptyBaseUrl() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Argument #2 ($baseUrl) cannot be empty'); + + Uri::fromRfc3986('https://example.com', ''); + } + + public function testParseInvalidUrl() + { + $uri = Uri::fromRfc3986("192.168/contact.html"); + + $this->assertSame('192.168/contact.html', $uri->getPath()); + } + + public function testInvalidCodePointInDomain() + { + $this->assertNull(Uri::fromRfc3986("http://RuPaul's Drag Race All Stars 7 Winners Cast on This Season's")); + } + + public function testConstructor() + { + $uri = new Rfc3986Uri("https://username:password@example.com:8080/path?q=r#fragment"); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('username', $uri->getUser()); + $this->assertSame('password', $uri->getPassword()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame(8080, $uri->getPort()); + $this->assertSame('path', $uri->getPath()); + $this->assertSame('q=r', $uri->getQuery()); + $this->assertSame('fragment', $uri->getFragment()); + } + + public function testIanaScheme() + { + $uri = Uri::fromRfc3986("chrome-extension://example.com"); + + $this->assertSame('chrome-extension', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + + $this->assertNull($uri->getUser()); + $this->assertNull($uri->getPassword()); + $this->assertNull($uri->getPort()); + $this->assertNull($uri->getPath()); + $this->assertNull($uri->getQuery()); + $this->assertNull($uri->getFragment()); + } + + public function testWithBaseUriAndAbsoluteUrl() + { + $uri = Uri::fromRfc3986("http://example.com/path/to/file2", "https://test.com"); + + $this->assertSame('http', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('path/to/file2', $uri->getPath()); + } + + public function testWithBaseUriAndRelativeUrl() + { + $uri = Uri::fromRfc3986("/path/to/file2", "https://test.com"); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('test.com', $uri->getHost()); + $this->assertSame('path/to/file2', $uri->getPath()); + } + + public function testNormalization() + { + $uri = Uri::fromRfc3986("HttPs://0300.0250.0000.0001/path?query=foo%20bar"); + + $this->assertSame('HttPs', $uri->getScheme()); + $this->assertSame('0300.0250.0000.0001', $uri->getHost()); + $this->assertSame('path', $uri->getPath()); + $this->assertSame('query=foo%20bar', $uri->getQuery()); + } + + public function testFileScheme() + { + $uri = Uri::fromRfc3986('file:///E:/Documents%20and%20Settings'); + + $this->assertSame('file', $uri->getScheme()); + $this->assertNull($uri->getHost()); + $this->assertSame('E:/Documents%20and%20Settings', $uri->getPath()); + } + + public function testInstantiateWithoutConstructor() + { + $r = new \ReflectionClass(Rfc3986Uri::class); + $uri = $r->newInstanceWithoutConstructor(); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Symfony\Polyfill\Php85\Uri\Rfc3986Uri object is not correctly initialized'); + + $uri->getHost(); + } + + public function testToString() + { + $uri = Uri::fromRfc3986('http://example.com?foo=Hell%C3%B3+W%C3%B6rld'); + + $this->assertSame('http://example.com?foo=Hell%C3%B3+W%C3%B6rld', $uri->__toString()); + } +} diff --git a/tests/Php85/WhatWgUriTest.php b/tests/Php85/WhatWgUriTest.php new file mode 100644 index 000000000..5f7e19236 --- /dev/null +++ b/tests/Php85/WhatWgUriTest.php @@ -0,0 +1,170 @@ +assertSame('https', $whatwg->getScheme()); + $this->assertSame('example.com', $whatwg->getHost()); + } + + public function testParseUrlWithElements() + { + $uri = Uri::fromWhatWg('https://username:password@example.com:8080/pathname1/pathname2/pathname3?query=true#hash-exists'); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('pathname1/pathname2/pathname3', $uri->getPath()); + $this->assertSame('query=true', $uri->getQuery()); + $this->assertSame('hash-exists', $uri->getFragment()); + } + + public function testParseExoticUrl() + { + $uri = Uri::fromWhatWg('http://username:password@héééostname:9090/gah/../path?arg=vaéue#anchor'); + + $this->assertSame('http', $uri->getScheme()); + $this->assertSame('xn--hostname-b1aaa', $uri->getHost()); + $this->assertSame(9090, $uri->getPort()); + $this->assertSame('path', $uri->getPath()); + $this->assertSame('arg=va%C3%A9ue', $uri->getQuery()); + $this->assertSame('anchor', $uri->getFragment()); + } + + public function testParsePath() + { + $this->assertNull(Uri::fromWhatWg("/page:1")); + } + + public function testEmptyUrl() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Argument #1 ($uri) cannot be empty'); + + Uri::fromWhatWg(''); + } + + public function testEmptyBaseUrl() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Argument #2 ($baseUrl) cannot be empty'); + + Uri::fromWhatWg('https://example.com/', ''); + } + + public function testParseInvalidUrl() + { + $errors = []; + $uri = Uri::fromWhatWg('192.168/contact.html', null, $errors); + + $this->assertNull($uri); + $this->assertSame('192.168/contact.html', $errors[0]->position); + $this->assertSame(WhatWgError::ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL, $errors[0]->errorCode); + } + + public function testInvalidCodePointInDomain() + { + $errors = []; + $uri = Uri::fromWhatWg("http://RuPaul's Drag Race All Stars 7 Winners Cast on This Season's", null, $errors); + + $this->assertNull($uri); + $this->assertSame("rupaul's drag race all stars 7 winners cast on this season's", $errors[0]->position); + $this->assertSame(WhatWgError::ERROR_TYPE_DOMAIN_INVALID_CODE_POINT, $errors[0]->errorCode); + } + + public function testConstructor() + { + $uri = new WhatWgUri("https://username:password@éxample.com:8080/path?q=r#fragment"); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('username', $uri->getUser()); + $this->assertSame('password', $uri->getPassword()); + $this->assertSame('xn--xample-9ua.com', $uri->getHost()); + $this->assertSame(8080, $uri->getPort()); + $this->assertSame('path', $uri->getPath()); + $this->assertSame('q=r', $uri->getQuery()); + $this->assertSame('fragment', $uri->getFragment()); + } + + public function testIanaScheme() + { + $uri = Uri::fromWhatWg("chrome-extension://example.com"); + + $this->assertSame('chrome-extension', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + + $this->assertNull($uri->getUser()); + $this->assertNull($uri->getPassword()); + $this->assertNull($uri->getPort()); + $this->assertNull($uri->getPath()); + $this->assertNull($uri->getQuery()); + $this->assertNull($uri->getFragment()); + } + + public function testWithBaseUriAndAbsoluteUrl() + { + $uri = Uri::fromWhatWg("http://example.com/path/to/file2", "https://test.com"); + + $this->assertSame('http', $uri->getScheme()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame('path/to/file2', $uri->getPath()); + } + + public function testWithBaseUriAndRelativeUrl() + { + $uri = Uri::fromWhatWg("/path/to/file2", "https://test.com"); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('test.com', $uri->getHost()); + $this->assertSame('path/to/file2', $uri->getPath()); + } + + public function testNormalization() + { + $uri = Uri::fromWhatWg("HttPs://0300.0250.0000.0001/path?query=foo%20bar"); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('1.0.168.192', $uri->getHost()); + $this->assertSame('path', $uri->getPath()); + $this->assertSame('query=foo+bar', $uri->getQuery()); + } + + public function testFileScheme() + { + $uri = Uri::fromWhatWg('file:///E:\\\\Documents and Settings'); + + $this->assertSame('file', $uri->getScheme()); + $this->assertNull($uri->getHost()); + $this->assertSame('E:/Documents%20and%20Settings', $uri->getPath()); + } + + public function testInstantiateWithoutConstructor() + { + $r = new \ReflectionClass(WhatWgUri::class); + $uri = $r->newInstanceWithoutConstructor(); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Symfony\Polyfill\Php85\Uri\WhatWgUri object is not correctly initialized'); + + $uri->getHost(); + } + + public function testToString() + { + $uri = Uri::fromWhatWg('http://user:pass@example.com?foo=Hell%C3%B3+W%C3%B6rld'); + + $this->assertSame('http://user:pass@example.com/?foo=Hell%C3%B3+W%C3%B6rld', $uri->__toString()); + } +}