From 98fe5c51c44dde2fe44756437fc269665ebcbb01 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 21 Sep 2026 15:57:17 +0800 Subject: [PATCH 1/2] ext/uri: fixed empty url credentials --- ...ssword_success_empty_string_with_base.phpt | 39 ++++++++++++ ..._string_with_empty_host_and_file_base.phpt | 50 ++++++++++++++++ ...d_success_empty_string_with_file_base.phpt | 40 +++++++++++++ ...success_empty_string_with_file_scheme.phpt | 37 ++++++++++++ ...y_string_with_localhost_and_file_base.phpt | 50 ++++++++++++++++ ...h_normalized_empty_host_and_file_base.phpt | 59 +++++++++++++++++++ ...ername_success_empty_string_with_base.phpt | 39 ++++++++++++ ..._string_with_empty_host_and_file_base.phpt | 50 ++++++++++++++++ ...e_success_empty_string_with_file_base.phpt | 40 +++++++++++++ ...success_empty_string_with_file_scheme.phpt | 37 ++++++++++++ ...y_string_with_localhost_and_file_base.phpt | 50 ++++++++++++++++ ...h_normalized_empty_host_and_file_base.phpt | 59 +++++++++++++++++++ ext/uri/uri_parser_whatwg.c | 17 ++++-- 13 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_empty_host_and_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_scheme.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_localhost_and_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_success_empty_string_with_normalized_empty_host_and_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_empty_host_and_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_scheme.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_localhost_and_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_success_empty_string_with_normalized_empty_host_and_file_base.phpt diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_base.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_base.phpt new file mode 100644 index 000000000000..d3480c0d82ac --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_base.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with base URL +--FILE-- +setPassword('') + ->build($base); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals(new Uri\WhatWg\Url('', $base), Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(38) "https://example.com/base/path?oldQuery" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.com" + ["port"]=> + NULL + ["path"]=> + string(10) "/base/path" + ["query"]=> + string(8) "oldQuery" + ["fragment"]=> + NULL +} +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_empty_host_and_file_base.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_empty_host_and_file_base.phpt new file mode 100644 index 000000000000..91477a21be6a --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_empty_host_and_file_base.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with empty host and file base URL +--FILE-- +setHost('') + ->setPassword('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(0) { +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_base.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_base.phpt new file mode 100644 index 000000000000..121595b9b830 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_base.phpt @@ -0,0 +1,40 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with file base URL +--FILE-- +setPassword('') + ->setHost('example.net') + ->build($base); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals(new Uri\WhatWg\Url('//example.net', $base), Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(19) "file://example.net/" +object(Uri\WhatWg\Url)#%d (8) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.net" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_scheme.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_scheme.phpt new file mode 100644 index 000000000000..085c2d66ff52 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_file_scheme.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with file scheme +--FILE-- +setScheme('file') + ->setPassword('') + ->setHost('example.net') + ->build(); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(19) "file://example.net/" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.net" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_localhost_and_file_base.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_localhost_and_file_base.phpt new file mode 100644 index 000000000000..5aa418282949 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_localhost_and_file_base.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with localhost and file base URL +--FILE-- +setHost('localhost') + ->setPassword('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(0) { +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/password_success_empty_string_with_normalized_empty_host_and_file_base.phpt b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_normalized_empty_host_and_file_base.phpt new file mode 100644 index 000000000000..f84710d23eb2 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_success_empty_string_with_normalized_empty_host_and_file_base.phpt @@ -0,0 +1,59 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - success - empty string with host normalised to empty and file base URL +--FILE-- +setHost("\t\n") + ->setPassword('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(2) "%r\x09\x0A%r" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + ["failure"]=> + bool(false) + } +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_base.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_base.phpt new file mode 100644 index 000000000000..0dced175a146 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_base.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with base URL +--FILE-- +setUsername('') + ->build($base); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals(new Uri\WhatWg\Url('', $base), Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(38) "https://example.com/base/path?oldQuery" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.com" + ["port"]=> + NULL + ["path"]=> + string(10) "/base/path" + ["query"]=> + string(8) "oldQuery" + ["fragment"]=> + NULL +} +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_empty_host_and_file_base.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_empty_host_and_file_base.phpt new file mode 100644 index 000000000000..40d72e0ef082 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_empty_host_and_file_base.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with empty host and file base URL +--FILE-- +setHost('') + ->setUsername('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(0) { +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_base.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_base.phpt new file mode 100644 index 000000000000..5b4075dc41d3 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_base.phpt @@ -0,0 +1,40 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with file base URL +--FILE-- +setUsername('') + ->setHost('example.net') + ->build($base); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals(new Uri\WhatWg\Url('//example.net', $base), Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(19) "file://example.net/" +object(Uri\WhatWg\Url)#%d (8) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.net" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_scheme.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_scheme.phpt new file mode 100644 index 000000000000..8456346e4089 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_file_scheme.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with file scheme +--FILE-- +setScheme('file') + ->setUsername('') + ->setHost('example.net') + ->build(); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); + +?> +--EXPECTF-- +string(19) "file://example.net/" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(11) "example.net" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_localhost_and_file_base.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_localhost_and_file_base.phpt new file mode 100644 index 000000000000..91136215741c --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_localhost_and_file_base.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with localhost and file base URL +--FILE-- +setHost('localhost') + ->setUsername('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(0) { +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/username_success_empty_string_with_normalized_empty_host_and_file_base.phpt b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_normalized_empty_host_and_file_base.phpt new file mode 100644 index 000000000000..79a6cc543ed9 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_success_empty_string_with_normalized_empty_host_and_file_base.phpt @@ -0,0 +1,59 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - success - empty string with host normalised to empty and file base URL +--FILE-- +setHost("\t\n") + ->setUsername('') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($errors == $referenceErrors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(8) "file:///" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(4) "file" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(0) "" + ["port"]=> + NULL + ["path"]=> + string(1) "/" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(2) "%r\x09\x0A%r" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + ["failure"]=> + bool(false) + } +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 1a258c6c940c..ef3e060e9224 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -29,9 +29,16 @@ ZEND_TLS uint8_t lexbor_custom_url_map[256] = {0}; static const size_t lexbor_mraw_byte_size = 8192; +ZEND_ATTRIBUTE_NONNULL static zend_always_inline bool zval_string_or_null_is_non_empty(const zval *value) +{ + ZEND_ASSERT(Z_ISNULL_P(value) || Z_TYPE_P(value) == IS_STRING); + + return Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0; +} + ZEND_ATTRIBUTE_NONNULL static zend_always_inline void zval_string_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str) { - if (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) { + if (zval_string_or_null_is_non_empty(value)) { lexbor_str->data = (lxb_char_t *) Z_STRVAL_P(value); lexbor_str->length = Z_STRLEN_P(value); } else { @@ -1071,12 +1078,12 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser } /* Credentials and ports require an authority in the reference itself. */ - if (Z_TYPE_P(username) == IS_STRING) { + if (zval_string_or_null_is_non_empty(username)) { php_uri_parser_whatwg_throw_exception("The specified URL cannot have username"); return NULL; } - if (Z_TYPE_P(password) == IS_STRING) { + if (zval_string_or_null_is_non_empty(password)) { php_uri_parser_whatwg_throw_exception("The specified URL cannot have password"); return NULL; } @@ -1306,12 +1313,12 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh if (lexbor_url->host.type == LXB_URL_HOST_TYPE__UNDEF || lexbor_url->host.type == LXB_URL_HOST_TYPE_EMPTY || lexbor_url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE) { - if (Z_TYPE_P(username) != IS_NULL) { + if (zval_string_or_null_is_non_empty(username)) { php_uri_parser_whatwg_throw_exception("The specified URL cannot have username"); goto failure; } - if (Z_TYPE_P(password) != IS_NULL) { + if (zval_string_or_null_is_non_empty(password)) { php_uri_parser_whatwg_throw_exception("The specified URL cannot have password"); goto failure; } From 5e2aa285df094fe78a69e54d9e7e73351b62cc24 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 21 Sep 2026 15:58:28 +0800 Subject: [PATCH 2/2] ext/uri: fixed file url authority resolution --- ...rror_invalid_authority_with_file_base.phpt | 50 +++++++++++ ...rror_invalid_authority_with_file_base.phpt | 48 ++++++++++ ...ssword_error_localhost_with_file_base.phpt | 48 ++++++++++ .../password_error_with_file_base.phpt | 48 ++++++++++ ...rror_invalid_authority_with_file_base.phpt | 48 ++++++++++ .../port_error_localhost_with_file_base.phpt | 48 ++++++++++ .../builder/port_error_with_file_base.phpt | 48 ++++++++++ ...ror_with_file_base_and_empty_username.phpt | 49 ++++++++++ ...ername_error_localhost_with_file_base.phpt | 48 ++++++++++ .../username_error_with_file_base.phpt | 48 ++++++++++ ext/uri/uri_parser_whatwg.c | 89 +++++++++++++++++++ 11 files changed, 572 insertions(+) create mode 100644 ext/uri/tests/whatwg/builder/build_error_invalid_authority_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_error_invalid_authority_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_error_localhost_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/password_error_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/port_error_invalid_authority_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/port_error_localhost_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/port_error_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/port_error_with_file_base_and_empty_username.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_error_localhost_with_file_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/username_error_with_file_base.phpt diff --git a/ext/uri/tests/whatwg/builder/build_error_invalid_authority_with_file_base.phpt b/ext/uri/tests/whatwg/builder/build_error_invalid_authority_with_file_base.phpt new file mode 100644 index 000000000000..259c580bb230 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_error_invalid_authority_with_file_base.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - error - invalid authority with file base URL +--FILE-- +setUsername('user') + ->setPassword('pass') + ->setHost('256.256.256.256') + ->setPort(124); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//user:pass@256.256.256.256:124', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified username is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(25) ":pass@256.256.256.256:124" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/password_error_invalid_authority_with_file_base.phpt b/ext/uri/tests/whatwg/builder/password_error_invalid_authority_with_file_base.phpt new file mode 100644 index 000000000000..c952a21b7492 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_error_invalid_authority_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - error - invalid authority with file base URL +--FILE-- +setPassword('newPass') + ->setHost('256.256.256.256'); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//:newPass@256.256.256.256', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified password is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(24) ":newpass@256.256.256.256" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/password_error_localhost_with_file_base.phpt b/ext/uri/tests/whatwg/builder/password_error_localhost_with_file_base.phpt new file mode 100644 index 000000000000..69d0f037268a --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_error_localhost_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - error - localhost with file base URL +--FILE-- +setPassword('newPass') + ->setHost('localhost'); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//:newPass@localhost', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified password is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(18) ":newpass@localhost" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/password_error_with_file_base.phpt b/ext/uri/tests/whatwg/builder/password_error_with_file_base.phpt new file mode 100644 index 000000000000..7a319714b2d3 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/password_error_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPassword() - error - with file base URL +--FILE-- +setPassword('newPassword') + ->setHost('example.net'); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//:newPassword@example.net', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified password is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(24) ":newpassword@example.net" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/port_error_invalid_authority_with_file_base.phpt b/ext/uri/tests/whatwg/builder/port_error_invalid_authority_with_file_base.phpt new file mode 100644 index 000000000000..9a80b4921340 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/port_error_invalid_authority_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPort() - error - invalid authority with file base URL +--FILE-- +setHost('256.256.256.256') + ->setPort(124); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//256.256.256.256:124', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified port is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(4) ":124" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/port_error_localhost_with_file_base.phpt b/ext/uri/tests/whatwg/builder/port_error_localhost_with_file_base.phpt new file mode 100644 index 000000000000..38731f45197f --- /dev/null +++ b/ext/uri/tests/whatwg/builder/port_error_localhost_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPort() - error - localhost with file base URL +--FILE-- +setHost('localhost') + ->setPort(124); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//localhost:124', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified port is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(4) ":124" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/port_error_with_file_base.phpt b/ext/uri/tests/whatwg/builder/port_error_with_file_base.phpt new file mode 100644 index 000000000000..79496c832f92 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/port_error_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPort() - error - with file base URL +--FILE-- +setHost('example.net') + ->setPort(124); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//example.net:124', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified port is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(4) ":124" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/port_error_with_file_base_and_empty_username.phpt b/ext/uri/tests/whatwg/builder/port_error_with_file_base_and_empty_username.phpt new file mode 100644 index 000000000000..c6c946e5fc0f --- /dev/null +++ b/ext/uri/tests/whatwg/builder/port_error_with_file_base_and_empty_username.phpt @@ -0,0 +1,49 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setPort() - error - with file base URL and empty username +--FILE-- +setUsername('') + ->setHost('example.net') + ->setPort(124); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//example.net:124', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified port is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(4) ":124" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/username_error_localhost_with_file_base.phpt b/ext/uri/tests/whatwg/builder/username_error_localhost_with_file_base.phpt new file mode 100644 index 000000000000..396abae3116f --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_error_localhost_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - error - localhost with file base URL +--FILE-- +setUsername('newUser') + ->setHost('localhost'); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//newUser@localhost', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified username is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(10) "@localhost" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/tests/whatwg/builder/username_error_with_file_base.phpt b/ext/uri/tests/whatwg/builder/username_error_with_file_base.phpt new file mode 100644 index 000000000000..72246d73ccdf --- /dev/null +++ b/ext/uri/tests/whatwg/builder/username_error_with_file_base.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::setUsername() - error - with file base URL +--FILE-- +setUsername('newUsername') + ->setHost('example.net'); + +$referenceErrors = null; + +foreach ([ + fn() => new Uri\WhatWg\Url('//newUsername@example.net', $base), + fn() => $builder->build($base), +] as $build) { + try { + $build(); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + + if ($referenceErrors === null) { + $referenceErrors = $e->errors; + continue; + } + + var_dump($referenceErrors == $e->errors); + var_dump($e->errors); + } +} + +?> +--EXPECTF-- +Uri\WhatWg\InvalidUrlException: The specified URI is malformed (DomainInvalidCodePoint) +Uri\WhatWg\InvalidUrlException: The specified username is malformed (DomainInvalidCodePoint) +bool(true) +array(1) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (3) { + ["context"]=> + string(12) "@example.net" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::DomainInvalidCodePoint) + ["failure"]=> + bool(true) + } +} diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index ef3e060e9224..64811577a6e3 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -1027,6 +1027,84 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_and_throw( } } +ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_append_userinfo( + smart_str *reference, const zval *value +) +{ + lexbor_str_t encoded = {0}; + const lxb_status_t status = lxb_url_percent_encode_utf_8( + (const lxb_char_t *) Z_STRVAL_P(value), Z_STRLEN_P(value), + &encoded, lexbor_parser.mraw, lxb_url_get_percent_encoding_map(), + LXB_URL_MAP_USERINFO, false + ); + if (status != LXB_STATUS_OK) { + lexbor_str_destroy(&encoded, lexbor_parser.mraw, false); + php_uri_parser_whatwg_throw_exception("Memory allocation error"); + return FAILURE; + } + + smart_str_appendl(reference, (const char *) encoded.data, encoded.length); + lexbor_str_destroy(&encoded, lexbor_parser.mraw, false); + + return SUCCESS; +} + +ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_validate_file_authority( + lxb_url_t *base_url, const zval *username, const zval *password, + const zval *host, const zval *port +) +{ + smart_str reference = {0}; + smart_str_appends(&reference, "//"); + + const bool has_username = zval_string_or_null_is_non_empty(username); + const bool has_password = zval_string_or_null_is_non_empty(password); + if (has_username || has_password) { + if (has_username && php_uri_parser_whatwg_append_userinfo(&reference, username) == FAILURE) { + goto failure; + } + if (has_password) { + smart_str_appendc(&reference, ':'); + if (php_uri_parser_whatwg_append_userinfo(&reference, password) == FAILURE) { + goto failure; + } + } + smart_str_appendc(&reference, '@'); + } + + smart_str_appendl(&reference, Z_STRVAL_P(host), Z_STRLEN_P(host)); + if (Z_TYPE_P(port) == IS_LONG) { + smart_str_appendc(&reference, ':'); + smart_str_append_long(&reference, Z_LVAL_P(port)); + } + smart_str_0(&reference); + + lxb_url_parser_clean(&lexbor_parser); + lxb_url_t *url = lxb_url_parse( + &lexbor_parser, base_url, + (const lxb_char_t *) ZSTR_VAL(reference.s), ZSTR_LEN(reference.s) + ); + if (url != NULL) { + lxb_url_destroy(url); + smart_str_free(&reference); + return SUCCESS; + } + + zval errors; + const char *reason = fill_errors(&errors); + const char *component = has_username + ? "username" + : (has_password ? "password" : "port"); + smart_str_free(&reference); + throw_invalid_url_exception_with_reason(NULL, component, reason, &errors); + + return FAILURE; + +failure: + smart_str_free(&reference); + return FAILURE; +} + /* 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) { @@ -1068,6 +1146,17 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser return NULL; } + if (lexbor_base_url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE + && (zval_string_or_null_is_non_empty(username) + || zval_string_or_null_is_non_empty(password) + || Z_TYPE_P(port) == IS_LONG) + && php_uri_parser_whatwg_validate_file_authority( + lexbor_base_url, username, password, host, port + ) == FAILURE + ) { + 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);