Commit a7a2303f957 for php.net
commit a7a2303f957c375ecf34bebdcfd319331e1dc76d
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date: Tue Sep 8 21:32:47 2026 +0200
ext/uri: Return builder soft errors consistently
Rename the output argument to softErrors and clear it on error-free success. Preserve it on failure while including earlier soft errors in the exception. This makes the behavior consistent with parsing in the constructor.
diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index 457e123bb48..9d6e58dc181 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -1404,12 +1404,12 @@ PHP_METHOD(Uri_WhatWg_UrlBuilder, setFragment)
PHP_METHOD(Uri_WhatWg_UrlBuilder, build)
{
zval *base_url_zv = NULL;
- zval *errors = NULL;
+ zval *soft_errors = NULL;
ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_OBJECT_OF_CLASS_OR_NULL(base_url_zv, php_uri_ce_whatwg_url)
- Z_PARAM_ZVAL(errors)
+ Z_PARAM_ZVAL(soft_errors)
ZEND_PARSE_PARAMETERS_END();
const zval *scheme = Z_WHATWG_URL_PROP_SCHEME_DEREF_P(ZEND_THIS);
@@ -1430,7 +1430,7 @@ PHP_METHOD(Uri_WhatWg_UrlBuilder, build)
lxb_url_t *lexbor_url = php_uri_parser_whatwg_build_from_zval(
base_url, scheme, username, password, host, port, path, query, fragment,
- errors
+ soft_errors
);
if (lexbor_url == NULL) {
RETURN_THROWS();
diff --git a/ext/uri/php_uri.stub.php b/ext/uri/php_uri.stub.php
index ad1d2fe32de..feca1871cea 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -240,8 +240,8 @@ public function setQuery(?string $query): static {}
public function setFragment(?string $fragment): static {}
- /** @param array $errors */
- public function build(?\Uri\WhatWg\Url $baseUrl = null, &$errors = null): \Uri\WhatWg\Url {}
+ /** @param array $softErrors */
+ public function build(?\Uri\WhatWg\Url $baseUrl = null, &$softErrors = null): \Uri\WhatWg\Url {}
}
/** @strict-properties */
diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index b9b4fedc10c..8139d4fbf7b 100644
Binary files a/ext/uri/php_uri_arginfo.h and b/ext/uri/php_uri_arginfo.h differ
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index a55b44a9520..04d12097833 100644
Binary files a/ext/uri/php_uri_decl.h and b/ext/uri/php_uri_decl.h differ
diff --git a/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt b/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt
new file mode 100644
index 00000000000..5cba13e51c1
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_error_soft_errors_unchanged.phpt
@@ -0,0 +1,46 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - error - leaves soft errors unchanged
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("ht\ttps");
+$builder->setHost(null);
+$softErrors = ["unchanged"];
+
+try {
+ $builder->build(softErrors: $softErrors);
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+ var_dump($e->errors);
+}
+
+var_dump($softErrors);
+
+?>
+--EXPECTF--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (HostMissing)
+array(2) {
+ [0]=>
+ object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+ ["context"]=>
+ string(4) " tps"
+ ["type"]=>
+ enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+ ["failure"]=>
+ bool(false)
+ }
+ [1]=>
+ object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+ ["context"]=>
+ string(0) ""
+ ["type"]=>
+ enum(Uri\WhatWg\UrlValidationErrorType::HostMissing)
+ ["failure"]=>
+ bool(true)
+ }
+}
+array(1) {
+ [0]=>
+ string(9) "unchanged"
+}
diff --git a/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt b/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt
new file mode 100644
index 00000000000..b2a860f03f5
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_success_soft_errors.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - success - returns soft errors
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("a\tb");
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($softErrors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(23) "https://example.com/#ab"
+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(2) "ab"
+}
+array(1) {
+ [0]=>
+ object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+ ["context"]=>
+ string(2) " b"
+ ["type"]=>
+ enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+ ["failure"]=>
+ bool(false)
+ }
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt b/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt
new file mode 100644
index 00000000000..4bfab1fd57e
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/build_success_soft_errors_reset.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::build() - success - clears soft errors from a previous build
+--FILE--
+<?php
+
+$builder = new Uri\WhatWg\UrlBuilder();
+$builder->setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("a\tb");
+$softErrors = [];
+$builder->build(softErrors: $softErrors);
+
+$builder->setFragment("ab");
+$url = $builder->build(softErrors: $softErrors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($softErrors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(23) "https://example.com/#ab"
+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(2) "ab"
+}
+array(0) {
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
index f4940291f75..8cbbee1f5df 100644
--- a/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
+++ b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
@@ -7,12 +7,12 @@
$builder->setScheme("foo");
$builder->setHost("example.com");
$builder->setFragment("\tfo\no");
-$errors = [];
-$url = $builder->build(errors: $errors);
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);
var_dump($url->toAsciiString());
var_dump($url);
-var_dump($errors);
+var_dump($softErrors);
var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
?>
diff --git a/ext/uri/tests/whatwg/builder/username_success_special_char.phpt b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
index 5e18a2c66cc..76122e8be00 100644
--- a/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
+++ b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
@@ -7,8 +7,8 @@
$builder->setScheme("https");
$builder->setHost("example.com");
$builder->setUsername("~%#");
-$errors = [];
-$url = $builder->build(null, $errors);
+$softErrors = [];
+$url = $builder->build(null, $softErrors);
var_dump($url->toAsciiString());
var_dump($url);
diff --git a/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
index 956bd1653ad..9e920033ab7 100644
--- a/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
+++ b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
@@ -7,12 +7,12 @@
$builder->setScheme("\tfo\no");
$builder->setHost("example.com");
$builder->setUsername("f\no\ro\t");
-$errors = [];
-$url = $builder->build(errors: $errors);
+$softErrors = [];
+$url = $builder->build(softErrors: $softErrors);
var_dump($url->toAsciiString());
var_dump($url);
-var_dump($errors);
+var_dump($softErrors);
var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
?>
diff --git a/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt b/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt
new file mode 100644
index 00000000000..826442fac19
--- /dev/null
+++ b/ext/uri/tests/whatwg/parsing/basic_success_reset.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\WhatWg\Url::parse() - success - clears errors when there are no validation errors
+--FILE--
+<?php
+
+$errors = ["previous error"];
+$url = Uri\WhatWg\Url::parse("https://example.org/", errors: $errors);
+
+var_dump($url);
+var_dump($url->toAsciiString());
+var_dump($errors);
+
+?>
+--EXPECTF--
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.org"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+string(20) "https://example.org/"
+array(0) {
+}
diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index c07e8fae900..f48f9205352 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -987,7 +987,7 @@ ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors(zval *erro
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 *errors_zv
+ zval *soft_errors_zv
) {
if (Z_TYPE_P(host) == IS_NULL ||
Z_STRLEN_P(host) == 0 ||
@@ -1028,7 +1028,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh
}
zval errors;
- ZVAL_UNDEF(&errors);
+ array_init(&errors);
zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL);
php_uri_parser_whatwg_build_errors(&errors);
@@ -1085,13 +1085,32 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_wh
/* TODO */
}
- if (php_uri_pass_errors_by_ref_and_free(errors_zv, &errors) == FAILURE) {
- 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:
+ /* Include errors from earlier components in the exception raised by a later component. */
+ if (zend_hash_num_elements(Z_ARRVAL(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(errors), error);
+ } ZEND_HASH_FOREACH_END();
+
+ zval_ptr_dtor(exception_errors);
+ ZVAL_COPY(exception_errors, &errors);
+ }
zval_ptr_dtor(&errors);
lxb_url_destroy(lexbor_url);
return NULL;
diff --git a/ext/uri/uri_parser_whatwg.h b/ext/uri/uri_parser_whatwg.h
index ed19a1067a5..360e8a2cd56 100644
--- a/ext/uri/uri_parser_whatwg.h
+++ b/ext/uri/uri_parser_whatwg.h
@@ -33,7 +33,7 @@ ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_port_validate(zend_long
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 *errors_zv
+ zval *soft_errors_zv
);
ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_userinfo_percent_encode(const char *str, size_t str_length);