diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c index aab94e946185..bdf1201e425e 100644 --- a/ext/uri/php_uri.c +++ b/ext/uri/php_uri.c @@ -1425,8 +1425,6 @@ PHP_METHOD(Uri_WhatWg_UrlBuilder, build) lxb_url_t *base_url = NULL; if (base_url_zv != NULL) { - zend_argument_error(NULL, 1, "is not supported yet, and therefore, null must be passed"); - RETURN_THROWS(); base_url = Z_URI_OBJECT_P(base_url_zv)->uri; } diff --git a/ext/uri/tests/whatwg/builder/authority_success_with_base.phpt b/ext/uri/tests/whatwg/builder/authority_success_with_base.phpt new file mode 100644 index 000000000000..ad8bc581e119 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/authority_success_with_base.phpt @@ -0,0 +1,21 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - replaces the base authority with encoded credentials +--FILE-- +setHost("example.net"); +$builder->setUsername("a@b"); +$builder->setPassword("c:d"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($base->toAsciiString()); + +?> +--EXPECT-- +string(32) "https://a%40b:c%3Ad@example.net/" +bool(true) +string(43) "https://old:secret@example.com:81/a?old#old" diff --git a/ext/uri/tests/whatwg/builder/basic_error_empty_reference_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/basic_error_empty_reference_with_opaque_base.phpt new file mode 100644 index 000000000000..d3fa225a61bc --- /dev/null +++ b/ext/uri/tests/whatwg/builder/basic_error_empty_reference_with_opaque_base.phpt @@ -0,0 +1,19 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - empty reference with an opaque base +--FILE-- +build($base); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + var_dump($e->errors[0]->type); +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified path is malformed (MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) diff --git a/ext/uri/tests/whatwg/builder/basic_error_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/basic_error_with_opaque_base.phpt new file mode 100644 index 000000000000..3e73978b86e8 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/basic_error_with_opaque_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder basic - error - with base URL containing opaque path +--FILE-- +setPath("/foo/bar/baz"); + +try { + $builder->build(new Uri\WhatWg\Url("scheme:opaque-path")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified path is malformed (MissingSchemeNonRelativeUrl) diff --git a/ext/uri/tests/whatwg/builder/basic_success_empty_reference_with_base.phpt b/ext/uri/tests/whatwg/builder/basic_success_empty_reference_with_base.phpt new file mode 100644 index 000000000000..6d40f4b96f8c --- /dev/null +++ b/ext/uri/tests/whatwg/builder/basic_success_empty_reference_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - inherits the path and query but not the fragment +--FILE-- +build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(25) "https://example.com/a?old" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt b/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt index 1fc354581db6..fcf4c42085e5 100644 --- a/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt +++ b/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt @@ -1,7 +1,5 @@ --TEST-- Test Uri\WhatWg\UrlBuilder basic - success - with base URL ---XFAIL-- -Support for passing $baseUrl to Uri\WhatWg\UrlBuilder::build() is not implemented yet. --FILE-- setHost("example.net"); +$builder->setPath("/foo/bar/baz"); +$builder->setPort(124); +$url = $builder->build(new Uri\WhatWg\Url("https://user:pass@example.com:123/foo/bar?query#hash")); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(35) "https://example.net:124/foo/bar/baz" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.net" + ["port"]=> + int(124) + ["path"]=> + string(12) "/foo/bar/baz" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_with_base.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_with_base.phpt new file mode 100644 index 000000000000..b1a8b221849e --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_with_base.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - preserves soft errors output with an opaque base URL +--FILE-- +setPath("/a\tb"); +$softErrors = ["previous error"]; + +try { + $builder->build(new Uri\WhatWg\Url("foo:opaque"), $softErrors); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + foreach ($e->errors as $error) { + var_dump($error->type); + } +} + +var_dump($softErrors); + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified path is malformed (MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) +array(0) { +} diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt similarity index 100% rename from ext/uri/tests/whatwg/builder/build_error_soft_errors_reset.phpt rename to ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_typed_reference_with_base.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_typed_reference_with_base.phpt new file mode 100644 index 000000000000..10b7e101dc96 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_typed_reference_with_base.phpt @@ -0,0 +1,23 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - soft errors assigned to a typed property with a base URL +--FILE-- +setFragment("a\tb"); +try { + $builder->build(new Uri\WhatWg\Url("https://example.com/"), $foo->errors); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} +var_dump($foo->errors); + +?> +--EXPECT-- +TypeError: Cannot assign array to reference held by property Foo::$errors of type string +string(9) "unchanged" diff --git a/ext/uri/tests/whatwg/builder/build_success_soft_errors_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_soft_errors_with_base.phpt new file mode 100644 index 000000000000..7f2b37146627 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_success_soft_errors_with_base.phpt @@ -0,0 +1,46 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - collects and resets soft errors with a base URL +--FILE-- +setPath("/a\tb"); +$builder->setFragment("x\ny"); +$base = new Uri\WhatWg\Url("https://example.com/"); +$softErrors = ["previous error"]; +$url = $builder->build($base, $softErrors); + +var_dump($url->toAsciiString()); +var_dump($softErrors); + +$builder->setPath("/ab"); +$builder->setFragment("xy"); +$builder->build($base, $softErrors); +var_dump($softErrors); + +?> +--EXPECTF-- +string(25) "https://example.com/ab#xy" +array(2) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(2) " b" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + ["failure"]=> + bool(false) + } + [1]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(2) " +y" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + ["failure"]=> + bool(false) + } +} +array(0) { +} diff --git a/ext/uri/tests/whatwg/builder/fragment_success_empty_with_base.phpt b/ext/uri/tests/whatwg/builder/fragment_success_empty_with_base.phpt new file mode 100644 index 000000000000..04f92a8f7764 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/fragment_success_empty_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - replaces the base fragment with an empty fragment +--FILE-- +setFragment(""); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(26) "https://example.com/a?old#" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/fragment_success_empty_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/fragment_success_empty_with_opaque_base.phpt new file mode 100644 index 000000000000..bcdb4d277b97 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/fragment_success_empty_with_opaque_base.phpt @@ -0,0 +1,21 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - resolves an empty fragment against an opaque base +--FILE-- +setPath("\t\n"); +$builder->setFragment(""); +$url = $builder->build($base, $softErrors); +var_dump($url->toAsciiString()); +var_dump($softErrors[0]->type); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($base->toAsciiString()); + +?> +--EXPECT-- +string(15) "foo:opaque?old#" +enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) +bool(true) +string(18) "foo:opaque?old#old" diff --git a/ext/uri/tests/whatwg/builder/fragment_success_with_base.phpt b/ext/uri/tests/whatwg/builder/fragment_success_with_base.phpt new file mode 100644 index 000000000000..7d2a84b59eaa --- /dev/null +++ b/ext/uri/tests/whatwg/builder/fragment_success_with_base.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setFragment() - success - with base URL +--FILE-- +setFragment("foo"); +$url = $builder->build(new Uri\WhatWg\Url("https://example.com/#bar")); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(24) "https://example.com/#foo" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.com" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + string(3) "foo" +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/fragment_success_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/fragment_success_with_opaque_base.phpt new file mode 100644 index 000000000000..53e0af4f66f6 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/fragment_success_with_opaque_base.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setFragment() - success - with base URL with opaque path +--FILE-- +setFragment("foo"); +$url = $builder->build(new Uri\WhatWg\Url("scheme:opaque-path")); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(22) "scheme:opaque-path#foo" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(6) "scheme" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + NULL + ["port"]=> + NULL + ["path"]=> + string(11) "opaque-path" + ["query"]=> + NULL + ["fragment"]=> + string(3) "foo" +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/host_error_normalized_empty_with_base.phpt b/ext/uri/tests/whatwg/builder/host_error_normalized_empty_with_base.phpt new file mode 100644 index 000000000000..7c16d8d57060 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/host_error_normalized_empty_with_base.phpt @@ -0,0 +1,22 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - rejects credentials after host normalization with a base URL +--FILE-- +setHost("\t"); +$builder->setUsername("user"); + +try { + $builder->build(new Uri\WhatWg\Url("foo://example.com/")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + foreach ($e->errors as $error) { + var_dump($error->type); + } +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified URL cannot have username +enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) diff --git a/ext/uri/tests/whatwg/builder/host_error_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/host_error_with_opaque_base.phpt new file mode 100644 index 000000000000..5194b7e6b6ff --- /dev/null +++ b/ext/uri/tests/whatwg/builder/host_error_with_opaque_base.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - new authority with an opaque base +--FILE-- +setHost("example.com"); + +try { + $builder->build($base); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + var_dump($e->errors[0]->type); +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified host is malformed (MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) diff --git a/ext/uri/tests/whatwg/builder/password_error_with_base.phpt b/ext/uri/tests/whatwg/builder/password_error_with_base.phpt new file mode 100644 index 000000000000..c6ade2d69d33 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_error_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - error - missing opaque host with base URL +--FILE-- +setPassword("password"); + +try { + $builder->build(new Uri\WhatWg\Url("https://example.com")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified URL cannot have password diff --git a/ext/uri/tests/whatwg/builder/path_error_tab_newline_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/path_error_tab_newline_with_opaque_base.phpt new file mode 100644 index 000000000000..0f53f745487f --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_error_tab_newline_with_opaque_base.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - ignored path characters without a fragment and with an opaque base +--FILE-- +setPath("\t\n"); + +try { + $builder->build($base); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + var_dump($e->errors[0]->type); +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified path is malformed (MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) diff --git a/ext/uri/tests/whatwg/builder/path_success_absolute_with_file_base.phpt b/ext/uri/tests/whatwg/builder/path_success_absolute_with_file_base.phpt new file mode 100644 index 000000000000..92a85ff9d15a --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_absolute_with_file_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - inherits the file drive for an absolute path +--FILE-- +setPath("/c"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(12) "file:///C:/c" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_drive_letter_with_file_base.phpt b/ext/uri/tests/whatwg/builder/path_success_drive_letter_with_file_base.phpt new file mode 100644 index 000000000000..7d15133b421c --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_drive_letter_with_file_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - replaces the file drive +--FILE-- +setPath("D:/c"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(12) "file:///D:/c" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_first_segment_colon_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_first_segment_colon_with_base.phpt new file mode 100644 index 000000000000..6e56b4c75f80 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_first_segment_colon_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - keeps a colon in the first path segment +--FILE-- +setPath("c:d"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(25) "https://example.com/a/c:d" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_hierarchical_delimiters_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_hierarchical_delimiters_with_base.phpt new file mode 100644 index 000000000000..ea942178d055 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_hierarchical_delimiters_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - keeps question mark and hashmark in the path +--FILE-- +setPath("a?b#c"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(31) "https://example.com/a/a%3Fb%23c" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_base.phpt new file mode 100644 index 000000000000..b38ec002d795 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - keeps a leading double slash in the path +--FILE-- +setPath("//other/x"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(28) "https://example.com//other/x" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_file_base.phpt b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_file_base.phpt new file mode 100644 index 000000000000..4b1cbf1289ce --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash_with_file_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - keeps a leading double slash in a file path +--FILE-- +setPath("//other/x"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(16) "file:////other/x" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_parent_segment_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_parent_segment_with_base.phpt new file mode 100644 index 000000000000..9c418b25ae60 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_parent_segment_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - resolves a parent path segment +--FILE-- +setPath("../c"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(21) "https://example.com/c" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_relative_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_relative_with_base.phpt new file mode 100644 index 000000000000..cf8ee60288a5 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_relative_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - resolves a path against the base directory and clears its query and fragment +--FILE-- +setPath("c"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(36) "https://user:pass@example.com:81/a/c" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/path_success_tab_newline_with_base.phpt b/ext/uri/tests/whatwg/builder/path_success_tab_newline_with_base.phpt new file mode 100644 index 000000000000..ce299716c0a8 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/path_success_tab_newline_with_base.phpt @@ -0,0 +1,19 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - inherits the path and query after removing tabs and newlines +--FILE-- +setPath("\t\n"); +$url = $builder->build($base, $softErrors); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($softErrors[0]->type); + +?> +--EXPECT-- +string(25) "https://example.com/a?old" +bool(true) +enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) diff --git a/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host_with_base.phpt b/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host_with_base.phpt new file mode 100644 index 000000000000..dc6b021f0841 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPort() - error - missing opaque host with base URL +--FILE-- +setPort(123); + +try { + $builder->build(new Uri\WhatWg\Url("https://example.com")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified URL cannot have port diff --git a/ext/uri/tests/whatwg/builder/query_error_empty_with_opaque_base.phpt b/ext/uri/tests/whatwg/builder/query_error_empty_with_opaque_base.phpt new file mode 100644 index 000000000000..f970ba3081ca --- /dev/null +++ b/ext/uri/tests/whatwg/builder/query_error_empty_with_opaque_base.phpt @@ -0,0 +1,22 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - empty query with an opaque base and ignored path characters +--FILE-- +setPath("\t\n"); +$builder->setQuery(""); +$builder->setFragment("new"); + +try { + $builder->build($base); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + var_dump($e->errors[0]->type); +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified query is malformed (MissingSchemeNonRelativeUrl) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) diff --git a/ext/uri/tests/whatwg/builder/query_success_empty_with_base.phpt b/ext/uri/tests/whatwg/builder/query_success_empty_with_base.phpt new file mode 100644 index 000000000000..e2e41cb7a127 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/query_success_empty_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - replaces the base query with an empty query +--FILE-- +setQuery(""); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(22) "https://example.com/a?" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/query_success_hashmark_with_base.phpt b/ext/uri/tests/whatwg/builder/query_success_hashmark_with_base.phpt new file mode 100644 index 000000000000..ef17d3e94104 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/query_success_hashmark_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - keeps a hashmark in the query +--FILE-- +setQuery("a#b"); +$url = $builder->build($base); + +var_dump($url->toAsciiString()); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECT-- +string(27) "https://example.com/a?a%23b" +bool(true) diff --git a/ext/uri/tests/whatwg/builder/query_success_with_base.phpt b/ext/uri/tests/whatwg/builder/query_success_with_base.phpt new file mode 100644 index 000000000000..5dc39a92ecf2 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/query_success_with_base.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setQuery() - success - with base URL +--FILE-- +setQuery("foo=bar"); +$url = $builder->build(new Uri\WhatWg\Url("https://example.com/?baz")); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(28) "https://example.com/?foo=bar" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.com" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + string(7) "foo=bar" + ["fragment"]=> + NULL +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_error_with_base.phpt b/ext/uri/tests/whatwg/builder/username_error_with_base.phpt new file mode 100644 index 000000000000..f58703309451 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_error_with_base.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - error - missing opaque host with base URL +--FILE-- +setUsername("username"); + +try { + $builder->build(new Uri\WhatWg\Url("https://example.com")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified URL cannot have username diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 8fee839b1cc4..1a258c6c940c 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -203,10 +203,8 @@ ZEND_ATTRIBUTE_NONNULL static const char *fill_errors(zval *errors) return fill_errors_inner(Z_ARRVAL_P(errors)); } -static void throw_invalid_url_exception_during_write(zval *errors, const char *component) +static void throw_invalid_url_exception_with_reason(zval *errors, const char *component, const char *reason, zval *err) { - zval err; - const char *reason = fill_errors(&err); zend_object *exception = zend_throw_exception_ex( php_uri_ce_whatwg_invalid_url_exception, 0, @@ -216,15 +214,23 @@ static void throw_invalid_url_exception_during_write(zval *errors, const char *c reason ? reason : "", reason ? ")" : "" ); - zend_update_property(exception->ce, exception, ZEND_STRL("errors"), &err); + zend_update_property(exception->ce, exception, ZEND_STRL("errors"), err); if (errors) { zval_ptr_dtor(errors); - ZVAL_COPY_VALUE(errors, &err); + ZVAL_COPY_VALUE(errors, err); } else { - zval_ptr_dtor(&err); + zval_ptr_dtor(err); } } +static void throw_invalid_url_exception_during_write(zval *errors, const char *component) +{ + zval err; + const char *reason = fill_errors(&err); + + throw_invalid_url_exception_with_reason(errors, component, reason, &err); +} + static lxb_status_t serialize_to_smart_str_callback(const lxb_char_t *data, const size_t length, void *ctx) { smart_str *uri_str = ctx; @@ -968,12 +974,12 @@ ZEND_ATTRIBUTE_NONNULL static lxb_url_scheme_type_t php_uri_parser_whatwg_get_sp return LXB_URL_SCHEMEL_TYPE__UNDEF; } -ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors(zval *errors) +ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_errors(zval *errors) { size_t log_len; if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) { - return; + return NULL; } if (Z_TYPE_P(errors) != IS_ARRAY) { @@ -981,7 +987,222 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors(zval *erro array_init_size(errors, log_len); } - fill_errors_inner(Z_ARRVAL_P(errors)); + return fill_errors_inner(Z_ARRVAL_P(errors)); +} + +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_exception(zval *errors) +{ + /* Include errors from earlier components in the exception raised by a later component. */ + if (zend_hash_num_elements(Z_ARRVAL_P(errors)) > 0 && EG(exception) + && instanceof_function(EG(exception)->ce, php_uri_ce_whatwg_invalid_url_exception)) { + zval rv; + zval *exception_errors = zend_read_property(php_uri_ce_whatwg_invalid_url_exception, + EG(exception), ZEND_STRL("errors"), true, &rv); + ZEND_ASSERT(Z_TYPE_P(exception_errors) == IS_ARRAY); + + zval *error; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), error) { + Z_TRY_ADDREF_P(error); + zend_hash_next_index_insert(Z_ARRVAL_P(errors), error); + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(exception_errors); + ZVAL_COPY(exception_errors, errors); + } +} + +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_and_throw(const lxb_status_t status, const char *component, zval *errors) +{ + if (status != LXB_STATUS_OK) { + throw_invalid_url_exception_during_write(NULL, component); + } else { + php_uri_parser_whatwg_build_errors(errors); + } +} + +/* TODO: Replace with lxb_url_path_set_null() once https://github.com/lexbor/lexbor/pull/415 is available. */ +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_path_set_null(lxb_url_t *url) +{ + if (url->path.str.data == NULL) { + return; + } + + (void) lexbor_str_destroy(&url->path.str, url->mraw, false); + + url->path.str.length = 0; + url->path.length = 0; + url->path.opaque = false; +} + +/* TODO: Replace with lxb_url_query_set_null() once https://github.com/lexbor/lexbor/pull/415 is available. */ +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_query_set_null(lxb_url_t *url) +{ + if (url->query.data != NULL) { + (void) lexbor_str_destroy(&url->query, url->mraw, false); + } +} + +/* TODO: Replace with lxb_url_fragment_set_null() once https://github.com/lexbor/lexbor/pull/415 is available. */ +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_fragment_set_null(lxb_url_t *url) +{ + if (url->fragment.data != NULL) { + (void) lexbor_str_destroy(&url->fragment, url->mraw, false); + } +} + +ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_resolve_reference_from_zval( + lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password, + const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment, + zval *soft_errors_zv +) { + if (Z_TYPE_P(host) == IS_STRING) { + if (lexbor_base_url->path.opaque) { + php_uri_parser_whatwg_component_error("host", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL); + return NULL; + } + + /* A new authority inherits only the scheme, not the base URL's other components. */ + zval base_scheme; + php_uri_parser_whatwg_scheme_read(lexbor_base_url, PHP_URI_COMPONENT_READ_MODE_NORMALIZED_ASCII, &base_scheme); + lxb_url_t *url = php_uri_parser_whatwg_build_from_zval(NULL, &base_scheme, + username, password, host, port, path, query, fragment, soft_errors_zv); + zval_ptr_dtor(&base_scheme); + return url; + } + + /* Credentials and ports require an authority in the reference itself. */ + if (Z_TYPE_P(username) == IS_STRING) { + php_uri_parser_whatwg_throw_exception("The specified URL cannot have username"); + return NULL; + } + + if (Z_TYPE_P(password) == IS_STRING) { + php_uri_parser_whatwg_throw_exception("The specified URL cannot have password"); + return NULL; + } + + if (Z_TYPE_P(port) == IS_LONG) { + php_uri_parser_whatwg_throw_exception("The specified URL cannot have port"); + return NULL; + } + + const char *first = Z_STRVAL_P(path); + const char *end = first + Z_STRLEN_P(path); + while (first < end && php_uri_whatwg_is_ascii_tab_or_newline(*first)) { + first++; + } + + lxb_status_t status; + zval errors; + array_init(&errors); + + lxb_url_t *lexbor_url = php_uri_parser_whatwg_clone(lexbor_base_url); + if (lexbor_url == NULL) { + zend_throw_exception(php_uri_ce_error, "Memory allocation error", 0); + goto failure; + } + if (lexbor_base_url->path.opaque && first == end + && (Z_TYPE_P(query) == IS_STRING || Z_TYPE_P(fragment) == IS_NULL)) { + php_uri_parser_whatwg_component_error(Z_TYPE_P(query) == IS_STRING ? "query" : "path", + LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL); + goto failure; + } + + /* Discard the base fragment; the reference fragment is applied below. */ + php_uri_parser_whatwg_fragment_set_null(lexbor_url); + + if (Z_STRLEN_P(path) > 0) { + /* Resolve relative paths against the base directory. Absolute paths use + * a path state so leading slashes cannot introduce a new authority. */ + lxb_url_state_t state = LXB_URL_STATE_NO_SCHEME_STATE; + if (!lexbor_base_url->path.opaque && first < end + && (*first == '/' || (lxb_url_is_special(lexbor_base_url) && *first == '\\'))) { + /* A path beginning with // must not replace the authority. */ + state = LXB_URL_STATE_PATH_START_STATE; + if (lexbor_base_url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE) { + const char *second = first + 1; + while (second < end && php_uri_whatwg_is_ascii_tab_or_newline(*second)) { + second++; + } + if (second == end || (*second != '/' && *second != '\\')) { + state = LXB_URL_STATE_FILE_STATE; + } + } + } + + /* Keep delimiters inside the path when entering the reference parser. */ + smart_str reference = {0}; + for (const char *p = Z_STRVAL_P(path); p < end; p++) { + if (*p == '?') { + smart_str_appends(&reference, "%3F"); + } else if (*p == '#') { + smart_str_appends(&reference, "%23"); + } else { + smart_str_appendc(&reference, *p); + } + } + + /* After removing tabs and newlines, the parser must see a fragment + * reference to accept an opaque base. Its actual value is applied below. */ + if (lexbor_base_url->path.opaque && first == end + && Z_TYPE_P(query) == IS_NULL && Z_TYPE_P(fragment) != IS_NULL) { + smart_str_appendc(&reference, '#'); + } + + zend_string *input = smart_str_extract(&reference); + php_uri_parser_whatwg_path_set_null(lexbor_url); + lxb_url_parser_clean(&lexbor_parser); + status = lxb_url_parse_basic(&lexbor_parser, lexbor_url, lexbor_base_url, + (const lxb_char_t *) ZSTR_VAL(input), ZSTR_LEN(input), state, LXB_ENCODING_UTF_8); + php_uri_parser_whatwg_build_errors_and_throw(status, "path", &errors); + zend_string_release(input); + if (status != LXB_STATUS_OK) { + goto failure; + } + if (first < end) { + php_uri_parser_whatwg_query_set_null(lexbor_url); + } + } + + if (Z_TYPE_P(query) == IS_STRING) { + php_uri_parser_whatwg_query_set_null(lexbor_url); + lxb_url_parser_clean(&lexbor_parser); + status = lxb_url_parse_basic(&lexbor_parser, lexbor_url, lexbor_base_url, + (lxb_char_t *) Z_STRVAL_P(query), Z_STRLEN_P(query), + LXB_URL_STATE_QUERY_STATE, LXB_ENCODING_AUTO + ); + php_uri_parser_whatwg_build_errors_and_throw(status, "query", &errors); + if (status != LXB_STATUS_OK) { + goto failure; + } + } + + if (Z_TYPE_P(fragment) == IS_STRING) { + php_uri_parser_whatwg_fragment_set_null(lexbor_url); + lxb_url_parser_clean(&lexbor_parser); + status = lxb_url_parse_basic(&lexbor_parser, lexbor_url, lexbor_base_url, + (lxb_char_t *) Z_STRVAL_P(fragment), Z_STRLEN_P(fragment), + LXB_URL_STATE_FRAGMENT_STATE, LXB_ENCODING_AUTO + ); + php_uri_parser_whatwg_build_errors_and_throw(status, "fragment", &errors); + if (status != LXB_STATUS_OK) { + goto failure; + } + } + + if (php_uri_pass_errors_by_ref_and_free(soft_errors_zv, &errors) == FAILURE) { + /* The errors zval was already consumed; goto failure would destroy it again. */ + lxb_url_destroy(lexbor_url); + return NULL; + } + + return lexbor_url; + +failure: + php_uri_parser_whatwg_build_errors_into_exception(&errors); + zval_ptr_dtor(&errors); + lxb_url_destroy(lexbor_url); + return NULL; } ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_build_path( @@ -1041,6 +1262,12 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh ) { lxb_url_parser_clean(&lexbor_parser); + if (lexbor_base_url != NULL && Z_TYPE_P(scheme) == IS_STRING && Z_STRLEN_P(scheme) == 0) { + return php_uri_parser_whatwg_resolve_reference_from_zval( + lexbor_base_url, scheme, username, password, host, port, path, query, fragment, soft_errors_zv + ); + } + lxb_url_t *lexbor_url = lexbor_mraw_calloc(lexbor_parser.mraw, sizeof(*lexbor_url)); if (lexbor_url == NULL) { zend_throw_exception(php_uri_ce_error, "Memory allocation error", 0); @@ -1148,10 +1375,6 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh } } - if (lexbor_base_url != NULL) { - /* TODO */ - } - if (php_uri_pass_errors_by_ref_and_free(soft_errors_zv, &errors) == FAILURE) { /* The errors zval was already consumed; goto failure would destroy it again. */ lxb_url_destroy(lexbor_url); @@ -1159,26 +1382,9 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh } return lexbor_url; - failure: ZEND_ASSERT(EG(exception)); - - /* Include errors from earlier components in the exception raised by a later component. */ - if (zend_hash_num_elements(Z_ARRVAL(errors)) > 0 - && instanceof_function(EG(exception)->ce, php_uri_ce_whatwg_invalid_url_exception)) { - zval rv; - zval *exception_errors = zend_read_property(php_uri_ce_whatwg_invalid_url_exception, - EG(exception), ZEND_STRL("errors"), true, &rv); - ZEND_ASSERT(Z_TYPE_P(exception_errors) == IS_ARRAY); - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), zval *error) { - Z_TRY_ADDREF_P(error); - zend_hash_next_index_insert(Z_ARRVAL(errors), error); - } ZEND_HASH_FOREACH_END(); - - zval_ptr_dtor(exception_errors); - ZVAL_COPY(exception_errors, &errors); - } + php_uri_parser_whatwg_build_errors_into_exception(&errors); zval_ptr_dtor(&errors); lxb_url_destroy(lexbor_url); return NULL;