From 24d1183a0750329f0ce15c1cb89d2e8751da6a2c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 10 Sep 2026 19:30:12 +0800 Subject: [PATCH 1/6] ext/uri: fixed validation order with new authorities --- ...ultiple_validation_warnings_with_base.phpt | 68 +++++++++++++++++++ ...ss_validation_warning_order_with_base.phpt | 67 ++++++++++++++++++ ext/uri/uri_parser_whatwg.c | 66 ++++++++++++++---- 3 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt create mode 100644 ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt diff --git a/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt new file mode 100644 index 000000000000..01ce4c586d21 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt @@ -0,0 +1,68 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - multiple validation warnings with base URL +--FILE-- +setHost('127.0.0.1.') + ->setPath('\newPath') + ->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(25) "https://127.0.0.1/newPath" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(9) "127.0.0.1" + ["port"]=> + NULL + ["path"]=> + string(8) "/newPath" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(2) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(8) "\newPath" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + ["failure"]=> + bool(false) + } + [1]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(0) "" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + ["failure"]=> + bool(false) + } +} +bool(true) +bool(true) +bool(true) diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt new file mode 100644 index 000000000000..27a266b46e86 --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt @@ -0,0 +1,67 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - validation warning order with base URL +--FILE-- +setUsername('newUser') + ->setPassword('newPass') + ->setHost('127.0.0.1.') + ->setPath('\newPath') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(41) "https://newUser:newPass@127.0.0.1/newPath" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + string(7) "newUser" + ["password"]=> + string(7) "newPass" + ["host"]=> + string(9) "127.0.0.1" + ["port"]=> + NULL + ["path"]=> + string(8) "/newPath" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(2) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(8) "\newPath" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + ["failure"]=> + bool(false) + } + [1]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(0) "" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + ["failure"]=> + bool(false) + } +} +bool(true) +bool(true) diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 2c123cd238d2..a9faa41bd278 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -990,6 +990,28 @@ ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_errors(zva return fill_errors_inner(Z_ARRVAL_P(errors)); } +ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_reference_errors(zval *errors) +{ + size_t log_len; + if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) { + return NULL; + } + + zval previous_errors; + ZVAL_COPY_VALUE(&previous_errors, errors); + array_init_size(errors, log_len + zend_hash_num_elements(Z_ARRVAL(previous_errors))); + + const char *reason = fill_errors_inner(Z_ARRVAL_P(errors)); + zval *error; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(previous_errors), error) { + Z_TRY_ADDREF_P(error); + zend_hash_next_index_insert(Z_ARRVAL_P(errors), error); + } ZEND_HASH_FOREACH_END(); + zval_ptr_dtor(&previous_errors); + + return reason; +} + 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. */ @@ -1050,6 +1072,12 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_fragment_set_null(lxb_u } } +ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_parser_whatwg_build_from_zval_ex( + 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, bool reference_errors +); + 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, @@ -1064,8 +1092,8 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser /* 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); + lxb_url_t *url = php_uri_parser_whatwg_build_from_zval_ex(NULL, &base_scheme, + username, password, host, port, path, query, fragment, soft_errors_zv, true); zval_ptr_dtor(&base_scheme); return url; } @@ -1259,10 +1287,10 @@ ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_build_path( return result; } -ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval( +ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_parser_whatwg_build_from_zval_ex( 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 + zval *soft_errors_zv, bool reference_errors ) { lxb_url_parser_clean(&lexbor_parser); @@ -1290,9 +1318,12 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh zval errors; array_init(&errors); + const char *(*build_errors)(zval *) = reference_errors + ? php_uri_parser_whatwg_build_reference_errors + : php_uri_parser_whatwg_build_errors; zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1301,7 +1332,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh * Otherwise, preserve the absent host so the path can be opaque. */ if (Z_TYPE_P(host) == IS_STRING || lxb_url_is_special(lexbor_url)) { result = php_uri_parser_whatwg_host_write(lexbor_url, host, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1328,27 +1359,27 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh /* Intentionally writing username after host to avoid error when the username is set but the host is missing */ result = php_uri_parser_whatwg_username_write(lexbor_url, username, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } /* Intentionally writing password after host to avoid error when the password is set but the password is missing */ result = php_uri_parser_whatwg_password_write(lexbor_url, password, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } /* Intentionally writing port after host to avoid error when the port is set but the host is missing */ result = php_uri_parser_whatwg_port_write(lexbor_url, port, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } result = php_uri_parser_whatwg_build_path(lexbor_url, path, query, fragment, &errors); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1360,7 +1391,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh lexbor_str_init(&lexbor_url->query, lexbor_url->mraw, 1); } else { result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1373,7 +1404,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh lexbor_str_init(&lexbor_url->fragment, lexbor_url->mraw, 1); } else { result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL); - php_uri_parser_whatwg_build_errors(&errors); + build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1394,6 +1425,17 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh return NULL; } +ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_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 +) { + return php_uri_parser_whatwg_build_from_zval_ex( + lexbor_base_url, scheme, username, password, host, port, path, query, fragment, + soft_errors_zv, false + ); +} + PHPAPI const php_uri_parser php_uri_parser_whatwg = { .name = PHP_URI_PARSER_WHATWG, .parse = php_uri_parser_whatwg_parse, From bf3651032eec035955f0897277e858310c827f5a Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 21 Sep 2026 20:28:57 +0800 Subject: [PATCH 2/6] ext/uri: fixed validation order with existing authorities --- .../build_success_soft_errors_with_base.phpt | 6 +- ...ing_order_without_authority_with_base.phpt | 71 +++++++++++++++++++ ext/uri/uri_parser_whatwg.c | 12 ++-- 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt 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 index 7f2b37146627..c4baff32ce59 100644 --- 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 @@ -25,7 +25,8 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " b" + string(2) " +y" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -34,8 +35,7 @@ array(2) { [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " -y" + string(2) " b" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt new file mode 100644 index 000000000000..785bb6fba40b --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - validation warning order without authority and with base URL +--FILE-- +setPath('/foo\bar') + ->setFragment('%') + ->build($base, $errors); + +var_dump($url->toAsciiString()); +var_dump($url); +var_dump($errors); +var_dump( + array_map(static fn($error) => $error->type, $errors) + === array_map(static fn($error) => $error->type, $referenceErrors) +); +var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); +var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); + +?> +--EXPECTF-- +string(29) "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(8) "/foo/bar" + ["query"]=> + NULL + ["fragment"]=> + string(1) "%" +} +array(2) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(1) "%" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + ["failure"]=> + bool(false) + } + [1]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(4) "\bar" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + ["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 a9faa41bd278..aab5fe3e20c6 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -1033,12 +1033,14 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_excep } } -ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_and_throw(const lxb_status_t status, const char *component, zval *errors) +ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_reference_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); + php_uri_parser_whatwg_build_reference_errors(errors); } } @@ -1186,7 +1188,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser 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); + php_uri_parser_whatwg_build_reference_errors_and_throw(status, "path", &errors); zend_string_release(input); if (status != LXB_STATUS_OK) { goto failure; @@ -1203,7 +1205,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser (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); + php_uri_parser_whatwg_build_reference_errors_and_throw(status, "query", &errors); if (status != LXB_STATUS_OK) { goto failure; } @@ -1216,7 +1218,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser (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); + php_uri_parser_whatwg_build_reference_errors_and_throw(status, "fragment", &errors); if (status != LXB_STATUS_OK) { goto failure; } From 8466337229d749bda31e3ee0db01a51992805076 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Mon, 21 Sep 2026 21:14:12 +0800 Subject: [PATCH 3/6] ext/uri: fixed standalone builder validation order --- ..._error_soft_errors_reset_without_base.phpt | 25 ++++-- ...validation_warning_order_without_base.phpt | 67 ++++++++++++++++ ext/uri/uri_parser_whatwg.c | 79 ++++++------------- 3 files changed, 110 insertions(+), 61 deletions(-) create mode 100644 ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt index f43ab31164bf..a9fa607b5a9f 100644 --- a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt @@ -3,6 +3,14 @@ Test Uri\WhatWg\UrlBuilder::build() - error - clears soft errors when an excepti --FILE-- errors; +} + $builder = new Uri\WhatWg\UrlBuilder(); $builder->setScheme("ht\ttps"); $builder->setHost(null); @@ -13,6 +21,10 @@ try { } catch (Throwable $e) { echo $e::class, ': ', $e->getMessage(), "\n"; var_dump($e->errors); + var_dump( + array_map(static fn($error) => $error->type, $e->errors) + === array_map(static fn($error) => $error->type, $referenceErrors) + ); } var_dump($softErrors); @@ -24,21 +36,22 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(4) " tps" + string(0) "" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + enum(Uri\WhatWg\UrlValidationErrorType::HostMissing) ["failure"]=> - bool(false) + bool(true) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(0) "" + string(4) " tps" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::HostMissing) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> - bool(true) + bool(false) } } +bool(true) array(0) { } diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt new file mode 100644 index 000000000000..8615b2a7546c --- /dev/null +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt @@ -0,0 +1,67 @@ +--TEST-- +Test Uri\WhatWg\UrlBuilder::build() - success - validation warning order without base URL +--FILE-- +setScheme('https') + ->setHost('127.0.0.1.') + ->setPath('\newPath') + ->build(null, $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(25) "https://127.0.0.1/newPath" +object(Uri\WhatWg\Url)#%d (%d) { + ["scheme"]=> + string(5) "https" + ["username"]=> + NULL + ["password"]=> + NULL + ["host"]=> + string(9) "127.0.0.1" + ["port"]=> + NULL + ["path"]=> + string(8) "/newPath" + ["query"]=> + NULL + ["fragment"]=> + NULL +} +array(2) { + [0]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(8) "\newPath" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + ["failure"]=> + bool(false) + } + [1]=> + object(Uri\WhatWg\UrlValidationError)#%d (%d) { + ["context"]=> + string(0) "" + ["type"]=> + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + ["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 aab5fe3e20c6..30a33b0ea139 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -976,23 +976,10 @@ ZEND_ATTRIBUTE_NONNULL static lxb_url_scheme_type_t php_uri_parser_whatwg_get_sp 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 NULL; - } - - if (Z_TYPE_P(errors) != IS_ARRAY) { - zval_ptr_dtor(errors); - array_init_size(errors, log_len); - } - - return fill_errors_inner(Z_ARRVAL_P(errors)); -} + ZEND_ASSERT(Z_TYPE_P(errors) == IS_ARRAY); -ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_reference_errors(zval *errors) -{ size_t log_len; + if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) { return NULL; } @@ -1021,26 +1008,22 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_excep 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); + SEPARATE_ARRAY(exception_errors); zval *error; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), error) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(errors), error) { Z_TRY_ADDREF_P(error); - zend_hash_next_index_insert(Z_ARRVAL_P(errors), error); + zend_hash_next_index_insert(Z_ARRVAL_P(exception_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_reference_errors_and_throw( - const lxb_status_t status, const char *component, zval *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_reference_errors(errors); + php_uri_parser_whatwg_build_errors(errors); } } @@ -1074,10 +1057,10 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_fragment_set_null(lxb_u } } -ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_parser_whatwg_build_from_zval_ex( +ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_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, bool reference_errors + zval *soft_errors_zv ); ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_resolve_reference_from_zval( @@ -1094,8 +1077,8 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser /* 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_ex(NULL, &base_scheme, - username, password, host, port, path, query, fragment, soft_errors_zv, true); + 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; } @@ -1188,7 +1171,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser 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_reference_errors_and_throw(status, "path", &errors); + php_uri_parser_whatwg_build_errors_and_throw(status, "path", &errors); zend_string_release(input); if (status != LXB_STATUS_OK) { goto failure; @@ -1205,7 +1188,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser (lxb_char_t *) Z_STRVAL_P(query), Z_STRLEN_P(query), LXB_URL_STATE_QUERY_STATE, LXB_ENCODING_AUTO ); - php_uri_parser_whatwg_build_reference_errors_and_throw(status, "query", &errors); + php_uri_parser_whatwg_build_errors_and_throw(status, "query", &errors); if (status != LXB_STATUS_OK) { goto failure; } @@ -1218,7 +1201,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser (lxb_char_t *) Z_STRVAL_P(fragment), Z_STRLEN_P(fragment), LXB_URL_STATE_FRAGMENT_STATE, LXB_ENCODING_AUTO ); - php_uri_parser_whatwg_build_reference_errors_and_throw(status, "fragment", &errors); + php_uri_parser_whatwg_build_errors_and_throw(status, "fragment", &errors); if (status != LXB_STATUS_OK) { goto failure; } @@ -1289,10 +1272,10 @@ ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_whatwg_build_path( return result; } -ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_parser_whatwg_build_from_zval_ex( +ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_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, bool reference_errors + zval *soft_errors_zv ) { lxb_url_parser_clean(&lexbor_parser); @@ -1320,12 +1303,9 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa zval errors; array_init(&errors); - const char *(*build_errors)(zval *) = reference_errors - ? php_uri_parser_whatwg_build_reference_errors - : php_uri_parser_whatwg_build_errors; zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1334,7 +1314,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa * Otherwise, preserve the absent host so the path can be opaque. */ if (Z_TYPE_P(host) == IS_STRING || lxb_url_is_special(lexbor_url)) { result = php_uri_parser_whatwg_host_write(lexbor_url, host, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1361,27 +1341,27 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa /* Intentionally writing username after host to avoid error when the username is set but the host is missing */ result = php_uri_parser_whatwg_username_write(lexbor_url, username, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } /* Intentionally writing password after host to avoid error when the password is set but the password is missing */ result = php_uri_parser_whatwg_password_write(lexbor_url, password, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } /* Intentionally writing port after host to avoid error when the port is set but the host is missing */ result = php_uri_parser_whatwg_port_write(lexbor_url, port, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } result = php_uri_parser_whatwg_build_path(lexbor_url, path, query, fragment, &errors); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1393,7 +1373,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa lexbor_str_init(&lexbor_url->query, lexbor_url->mraw, 1); } else { result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1406,7 +1386,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa lexbor_str_init(&lexbor_url->fragment, lexbor_url->mraw, 1); } else { result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL); - build_errors(&errors); + php_uri_parser_whatwg_build_errors(&errors); if (result == FAILURE) { goto failure; } @@ -1427,17 +1407,6 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) static lxb_url_t *php_uri_pa return NULL; } -ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_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 -) { - return php_uri_parser_whatwg_build_from_zval_ex( - lexbor_base_url, scheme, username, password, host, port, path, query, fragment, - soft_errors_zv, false - ); -} - PHPAPI const php_uri_parser php_uri_parser_whatwg = { .name = PHP_URI_PARSER_WHATWG, .parse = php_uri_parser_whatwg_parse, From 3562835e2f963b3cb3272fc39a4ff0488c912e42 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 22 Sep 2026 00:03:19 +0800 Subject: [PATCH 4/6] review: inlined zval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Düsterhus <209270+TimWolla@users.noreply.github.com> --- ext/uri/uri_parser_whatwg.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 30a33b0ea139..cd871edc5bb7 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -989,8 +989,7 @@ ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_errors(zva array_init_size(errors, log_len + zend_hash_num_elements(Z_ARRVAL(previous_errors))); const char *reason = fill_errors_inner(Z_ARRVAL_P(errors)); - zval *error; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(previous_errors), error) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(previous_errors), zval *error) { Z_TRY_ADDREF_P(error); zend_hash_next_index_insert(Z_ARRVAL_P(errors), error); } ZEND_HASH_FOREACH_END(); From 093767287c8de5ce0493180358eab23eeab2dfbe Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 7 Oct 2026 13:14:56 +0800 Subject: [PATCH 5/6] review: reversed error order --- UPGRADING | 3 ++ ...ild_error_soft_errors_reset_with_base.phpt | 2 +- ..._error_soft_errors_reset_without_base.phpt | 12 +++--- ...ultiple_validation_warnings_with_base.phpt | 8 ++-- .../build_success_soft_errors_with_base.phpt | 6 +-- ...ss_validation_warning_order_with_base.phpt | 8 ++-- ...ing_order_without_authority_with_base.phpt | 8 ++-- ...validation_warning_order_without_base.phpt | 8 ++-- .../builder/path_success_opaque_spaces.phpt | 4 +- ...h_success_opaque_spaces_with_fragment.phpt | 4 +- ...path_success_opaque_spaces_with_query.phpt | 4 +- ...ath_success_file_invalid_drive_letter.phpt | 4 +- ...sic_success_invalid_url_unit_warnings.phpt | 4 +- ext/uri/uri_parser_whatwg.c | 41 ++++++++----------- 14 files changed, 55 insertions(+), 61 deletions(-) diff --git a/UPGRADING b/UPGRADING index 0e4a5288d680..260b5ee1b1aa 100644 --- a/UPGRADING +++ b/UPGRADING @@ -326,6 +326,9 @@ PHP 8.6 UPGRADE NOTES system. - URI: + . Uri\WhatWg\Url and Uri\WhatWg\UrlBuilder now expose validation errors in + encounter order. The order of multiple errors may therefore change in + $softErrors and Uri\WhatWg\InvalidUrlException::$errors. . Uri\WhatWg\Url::__construct() now sets $softErrors to an empty array when URL processing throws an exception, instead of preserving its value. URL validation errors remain available in the 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 index b1a8b221849e..5ac520b2ebcf 100644 --- 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 @@ -21,7 +21,7 @@ var_dump($softErrors); ?> --EXPECT-- Uri\WhatWg\InvalidUrlException: The specified path is malformed (MissingSchemeNonRelativeUrl) -enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) +enum(Uri\WhatWg\UrlValidationErrorType::MissingSchemeNonRelativeUrl) array(0) { } diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt index a9fa607b5a9f..95f3d690afa0 100644 --- a/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_reset_without_base.phpt @@ -36,20 +36,20 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(0) "" + string(4) " tps" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::HostMissing) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> - bool(true) + bool(false) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(4) " tps" + string(0) "" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + enum(Uri\WhatWg\UrlValidationErrorType::HostMissing) ["failure"]=> - bool(false) + bool(true) } } bool(true) diff --git a/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt index 01ce4c586d21..92bebe75be0c 100644 --- a/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt @@ -47,18 +47,18 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(8) "\newPath" + string(0) "" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) ["failure"]=> bool(false) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(0) "" + string(8) "\newPath" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) ["failure"]=> bool(false) } 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 index c4baff32ce59..7f2b37146627 100644 --- 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 @@ -25,8 +25,7 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " -y" + string(2) " b" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -35,7 +34,8 @@ y" [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " b" + string(2) " +y" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt index 27a266b46e86..7393d1d17c08 100644 --- a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt @@ -47,18 +47,18 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(8) "\newPath" + string(0) "" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) ["failure"]=> bool(false) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(0) "" + string(8) "\newPath" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) ["failure"]=> bool(false) } diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt index 785bb6fba40b..3c23c90683ef 100644 --- a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt @@ -50,18 +50,18 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(1) "%" + string(4) "\bar" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) ["failure"]=> bool(false) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(4) "\bar" + string(1) "%" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> bool(false) } diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt index 8615b2a7546c..815d48c422c5 100644 --- a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt +++ b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_base.phpt @@ -46,18 +46,18 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(8) "\newPath" + string(0) "" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) + enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) ["failure"]=> bool(false) } [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(0) "" + string(8) "\newPath" ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) + enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) ["failure"]=> bool(false) } diff --git a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces.phpt b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces.phpt index c66d10f29f50..80c30d8a7b80 100644 --- a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces.phpt +++ b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces.phpt @@ -39,7 +39,7 @@ array(3) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(1) " " + string(6) " abc " ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -57,7 +57,7 @@ array(3) { [2]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(6) " abc " + string(1) " " ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_fragment.phpt b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_fragment.phpt index b6d72abded4c..f1bd6860c96a 100644 --- a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_fragment.phpt +++ b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_fragment.phpt @@ -40,7 +40,7 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " #" + string(3) " #" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -49,7 +49,7 @@ array(2) { [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(3) " #" + string(2) " #" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_query.phpt b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_query.phpt index d719feafc3d8..9a52a7c97651 100644 --- a/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_query.phpt +++ b/ext/uri/tests/whatwg/builder/path_success_opaque_spaces_with_query.phpt @@ -40,7 +40,7 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(2) " ?" + string(3) " ?" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -49,7 +49,7 @@ array(2) { [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(3) " ?" + string(2) " ?" ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/tests/whatwg/modification/withPath_success_file_invalid_drive_letter.phpt b/ext/uri/tests/whatwg/modification/withPath_success_file_invalid_drive_letter.phpt index 39ed5d30a4fc..f57c8b0da2a7 100644 --- a/ext/uri/tests/whatwg/modification/withPath_success_file_invalid_drive_letter.phpt +++ b/ext/uri/tests/whatwg/modification/withPath_success_file_invalid_drive_letter.phpt @@ -14,8 +14,8 @@ var_dump($url->withPath("/zz")->getPath()); string(5) "/c:/x" array(2) { [0]=> - string(14) "InvalidUrlUnit" - [1]=> string(29) "FileInvalidWindowsDriveLetter" + [1]=> + string(14) "InvalidUrlUnit" } string(3) "/zz" diff --git a/ext/uri/tests/whatwg/parsing/basic_success_invalid_url_unit_warnings.phpt b/ext/uri/tests/whatwg/parsing/basic_success_invalid_url_unit_warnings.phpt index 4ee2a3d3b921..db989449071e 100644 --- a/ext/uri/tests/whatwg/parsing/basic_success_invalid_url_unit_warnings.phpt +++ b/ext/uri/tests/whatwg/parsing/basic_success_invalid_url_unit_warnings.phpt @@ -35,7 +35,7 @@ array(2) { [0]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(1) " " + string(21) " https://example.org " ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> @@ -44,7 +44,7 @@ array(2) { [1]=> object(Uri\WhatWg\UrlValidationError)#%d (%d) { ["context"]=> - string(21) " https://example.org " + string(1) " " ["type"]=> enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) ["failure"]=> diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index cd871edc5bb7..2d2f4a81b8cd 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -173,14 +173,17 @@ ZEND_ATTRIBUTE_NONNULL static bool append_validation_error( ZEND_ATTRIBUTE_NONNULL static const char *fill_errors_inner(HashTable *errors) { const char *result = NULL; + lexbor_plog_t *log = lexbor_parser.log; + const size_t length = lexbor_plog_length(log); - lexbor_plog_entry_t *lxb_error; - while ((lxb_error = lexbor_array_obj_pop(&lexbor_parser.log->list)) != NULL) { + for (size_t i = 0; i < length; i++) { + const lexbor_plog_entry_t *lxb_error = lexbor_array_obj_get(&log->list, i); const char *reason; - if (append_validation_error(errors, lxb_error->id, (const char *) lxb_error->data, &reason)) { + if (append_validation_error(errors, lxb_error->id, (const char *) lxb_error->data, &reason) && result == NULL) { result = reason; } } + lexbor_plog_clean(log); return result; } @@ -976,26 +979,18 @@ ZEND_ATTRIBUTE_NONNULL static lxb_url_scheme_type_t php_uri_parser_whatwg_get_sp ZEND_ATTRIBUTE_NONNULL static const char *php_uri_parser_whatwg_build_errors(zval *errors) { - ZEND_ASSERT(Z_TYPE_P(errors) == IS_ARRAY); - size_t log_len; if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) { return NULL; } - zval previous_errors; - ZVAL_COPY_VALUE(&previous_errors, errors); - array_init_size(errors, log_len + zend_hash_num_elements(Z_ARRVAL(previous_errors))); - - const char *reason = fill_errors_inner(Z_ARRVAL_P(errors)); - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(previous_errors), zval *error) { - Z_TRY_ADDREF_P(error); - zend_hash_next_index_insert(Z_ARRVAL_P(errors), error); - } ZEND_HASH_FOREACH_END(); - zval_ptr_dtor(&previous_errors); + if (Z_TYPE_P(errors) != IS_ARRAY) { + zval_ptr_dtor(errors); + array_init_size(errors, log_len); + } - return reason; + return fill_errors_inner(Z_ARRVAL_P(errors)); } ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_exception(zval *errors) @@ -1007,13 +1002,15 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors_into_excep 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); - SEPARATE_ARRAY(exception_errors); zval *error; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(errors), error) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(exception_errors), error) { Z_TRY_ADDREF_P(error); - zend_hash_next_index_insert(Z_ARRVAL_P(exception_errors), 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); } } @@ -1056,12 +1053,6 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_fragment_set_null(lxb_u } } -ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_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 -); - 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, From b8c29b26661c6f1be20903f28f4eceeecfb7e1ad Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 7 Oct 2026 20:02:03 +0800 Subject: [PATCH 6/6] review: clarified error order and removed redundant tests --- UPGRADING | 7 +- ...ultiple_validation_warnings_with_base.phpt | 68 ------------------ ...ss_validation_warning_order_with_base.phpt | 67 ----------------- ...ing_order_without_authority_with_base.phpt | 71 ------------------- 4 files changed, 4 insertions(+), 209 deletions(-) delete mode 100644 ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt delete mode 100644 ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt delete mode 100644 ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt diff --git a/UPGRADING b/UPGRADING index 260b5ee1b1aa..d426f960abea 100644 --- a/UPGRADING +++ b/UPGRADING @@ -326,9 +326,10 @@ PHP 8.6 UPGRADE NOTES system. - URI: - . Uri\WhatWg\Url and Uri\WhatWg\UrlBuilder now expose validation errors in - encounter order. The order of multiple errors may therefore change in - $softErrors and Uri\WhatWg\InvalidUrlException::$errors. + . __construct(), parse(), and with*() of Uri\WhatWg\Url, and + Uri\WhatWg\UrlBuilder::build() now report validation errors in the order + in which they are detected. This reverses the order of multiple errors + compared to PHP 8.5. . Uri\WhatWg\Url::__construct() now sets $softErrors to an empty array when URL processing throws an exception, instead of preserving its value. URL validation errors remain available in the diff --git a/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt deleted file mode 100644 index 92bebe75be0c..000000000000 --- a/ext/uri/tests/whatwg/builder/build_success_multiple_validation_warnings_with_base.phpt +++ /dev/null @@ -1,68 +0,0 @@ ---TEST-- -Test Uri\WhatWg\UrlBuilder::build() - success - multiple validation warnings with base URL ---FILE-- -setHost('127.0.0.1.') - ->setPath('\newPath') - ->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(25) "https://127.0.0.1/newPath" -object(Uri\WhatWg\Url)#%d (%d) { - ["scheme"]=> - string(5) "https" - ["username"]=> - NULL - ["password"]=> - NULL - ["host"]=> - string(9) "127.0.0.1" - ["port"]=> - NULL - ["path"]=> - string(8) "/newPath" - ["query"]=> - NULL - ["fragment"]=> - NULL -} -array(2) { - [0]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(0) "" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) - ["failure"]=> - bool(false) - } - [1]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(8) "\newPath" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) - ["failure"]=> - bool(false) - } -} -bool(true) -bool(true) -bool(true) diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt deleted file mode 100644 index 7393d1d17c08..000000000000 --- a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_with_base.phpt +++ /dev/null @@ -1,67 +0,0 @@ ---TEST-- -Test Uri\WhatWg\UrlBuilder::build() - success - validation warning order with base URL ---FILE-- -setUsername('newUser') - ->setPassword('newPass') - ->setHost('127.0.0.1.') - ->setPath('\newPath') - ->build($base, $errors); - -var_dump($url->toAsciiString()); -var_dump($url); -var_dump($errors); -var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); -var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); - -?> ---EXPECTF-- -string(41) "https://newUser:newPass@127.0.0.1/newPath" -object(Uri\WhatWg\Url)#%d (%d) { - ["scheme"]=> - string(5) "https" - ["username"]=> - string(7) "newUser" - ["password"]=> - string(7) "newPass" - ["host"]=> - string(9) "127.0.0.1" - ["port"]=> - NULL - ["path"]=> - string(8) "/newPath" - ["query"]=> - NULL - ["fragment"]=> - NULL -} -array(2) { - [0]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(0) "" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::Ipv4EmptyPart) - ["failure"]=> - bool(false) - } - [1]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(8) "\newPath" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) - ["failure"]=> - bool(false) - } -} -bool(true) -bool(true) diff --git a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt b/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt deleted file mode 100644 index 3c23c90683ef..000000000000 --- a/ext/uri/tests/whatwg/builder/build_success_validation_warning_order_without_authority_with_base.phpt +++ /dev/null @@ -1,71 +0,0 @@ ---TEST-- -Test Uri\WhatWg\UrlBuilder::build() - success - validation warning order without authority and with base URL ---FILE-- -setPath('/foo\bar') - ->setFragment('%') - ->build($base, $errors); - -var_dump($url->toAsciiString()); -var_dump($url); -var_dump($errors); -var_dump( - array_map(static fn($error) => $error->type, $errors) - === array_map(static fn($error) => $error->type, $referenceErrors) -); -var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString()))); -var_dump($url->equals($reference, Uri\UriComparisonMode::IncludeFragment)); - -?> ---EXPECTF-- -string(29) "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(8) "/foo/bar" - ["query"]=> - NULL - ["fragment"]=> - string(1) "%" -} -array(2) { - [0]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(4) "\bar" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidReverseSoldius) - ["failure"]=> - bool(false) - } - [1]=> - object(Uri\WhatWg\UrlValidationError)#%d (%d) { - ["context"]=> - string(1) "%" - ["type"]=> - enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit) - ["failure"]=> - bool(false) - } -} -bool(true) -bool(true) -bool(true)