- Reverts the schneespur/ subdirectory restructure (b8e426b)
- Restores package.json and vite.config.js (needed for npm build, were
removed in an earlier cleanup before the restructure)
- Updates public/build/ assets with current Vite output (new content hashes)
1193 lines
44 KiB
PHP
1193 lines
44 KiB
PHP
<?php
|
||
|
||
/**
|
||
* League.Uri (https://uri.thephpleague.com)
|
||
*
|
||
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
|
||
*
|
||
* 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' => ['//:<3A>@<40><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/'],
|
||
];
|
||
}
|
||
|
||
#[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<list<string>>
|
||
*/
|
||
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));
|
||
}
|
||
}
|