From 8c1fb347cb3b926ccbe28e4cf20b3e78a3d62cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 26 Oct 2025 17:26:52 +0100 Subject: [PATCH 1/3] =?UTF-8?q?uri:=20Use=20the=20=E2=80=9Cincludes=20cred?= =?UTF-8?q?entials=E2=80=9D=20rule=20for=20WhatWg=20user/password=20getter?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The URL serializing algorithm from the WHATWG URL Standard uses an “includes credentials” rule to decide whether or not to include the `@` in the output, indicating the presence of a userinfo component in RFC 3986 terminology. Use this rule to determine whether or not an empty username or password should be returned as the empty string (present but empty) or NULL (not present). --- .../password_success_unset_existing.phpt | 2 +- .../password_success_unset_existing2.phpt | 19 +++++++++++++++++++ .../password_success_unset_non_existent2.phpt | 4 ++-- .../username_success_unset_existing.phpt | 2 +- .../username_success_unset_existing2.phpt | 19 +++++++++++++++++++ .../username_success_unset_non_existent2.phpt | 4 ++-- ext/uri/uri_parser_whatwg.c | 10 ++++++++-- 7 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 ext/uri/tests/whatwg/modification/password_success_unset_existing2.phpt create mode 100644 ext/uri/tests/whatwg/modification/username_success_unset_existing2.phpt diff --git a/ext/uri/tests/whatwg/modification/password_success_unset_existing.phpt b/ext/uri/tests/whatwg/modification/password_success_unset_existing.phpt index 957973b062a22..203b831411a66 100644 --- a/ext/uri/tests/whatwg/modification/password_success_unset_existing.phpt +++ b/ext/uri/tests/whatwg/modification/password_success_unset_existing.phpt @@ -15,5 +15,5 @@ var_dump($url2->toAsciiString()); ?> --EXPECT-- string(8) "password" -NULL +string(0) "" string(29) "https://username@example.com/" diff --git a/ext/uri/tests/whatwg/modification/password_success_unset_existing2.phpt b/ext/uri/tests/whatwg/modification/password_success_unset_existing2.phpt new file mode 100644 index 0000000000000..06bade29468fd --- /dev/null +++ b/ext/uri/tests/whatwg/modification/password_success_unset_existing2.phpt @@ -0,0 +1,19 @@ +--TEST-- +Test Uri\WhatWg\Url component modification - password - unsetting existing +--EXTENSIONS-- +uri +--FILE-- +withPassword(null); + +var_dump($url1->getPassword()); +var_dump($url2->getPassword()); +var_dump($url2->toAsciiString()); + +?> +--EXPECT-- +string(8) "password" +NULL +string(20) "https://example.com/" diff --git a/ext/uri/tests/whatwg/modification/password_success_unset_non_existent2.phpt b/ext/uri/tests/whatwg/modification/password_success_unset_non_existent2.phpt index a2140669ee8aa..a795f3197a6dc 100644 --- a/ext/uri/tests/whatwg/modification/password_success_unset_non_existent2.phpt +++ b/ext/uri/tests/whatwg/modification/password_success_unset_non_existent2.phpt @@ -14,6 +14,6 @@ var_dump($url2->toAsciiString()); ?> --EXPECT-- -NULL -NULL +string(0) "" +string(0) "" string(29) "https://username@example.com/" diff --git a/ext/uri/tests/whatwg/modification/username_success_unset_existing.phpt b/ext/uri/tests/whatwg/modification/username_success_unset_existing.phpt index f71deff3f207b..8340b4ca1db61 100644 --- a/ext/uri/tests/whatwg/modification/username_success_unset_existing.phpt +++ b/ext/uri/tests/whatwg/modification/username_success_unset_existing.phpt @@ -15,5 +15,5 @@ var_dump($url2->toAsciiString()); ?> --EXPECT-- string(8) "username" -NULL +string(0) "" string(30) "https://:password@example.com/" diff --git a/ext/uri/tests/whatwg/modification/username_success_unset_existing2.phpt b/ext/uri/tests/whatwg/modification/username_success_unset_existing2.phpt new file mode 100644 index 0000000000000..ccfeac222e4e9 --- /dev/null +++ b/ext/uri/tests/whatwg/modification/username_success_unset_existing2.phpt @@ -0,0 +1,19 @@ +--TEST-- +Test Uri\WhatWg\Url component modification - username - unsetting existing +--EXTENSIONS-- +uri +--FILE-- +withUsername(null); + +var_dump($url1->getUsername()); +var_dump($url2->getUsername()); +var_dump($url2->toAsciiString()); + +?> +--EXPECT-- +string(8) "username" +NULL +string(20) "https://example.com/" diff --git a/ext/uri/tests/whatwg/modification/username_success_unset_non_existent2.phpt b/ext/uri/tests/whatwg/modification/username_success_unset_non_existent2.phpt index e5af4efb223db..7886b35fd9b84 100644 --- a/ext/uri/tests/whatwg/modification/username_success_unset_non_existent2.phpt +++ b/ext/uri/tests/whatwg/modification/username_success_unset_non_existent2.phpt @@ -14,6 +14,6 @@ var_dump($url2->toAsciiString()); ?> --EXPECT-- -NULL -NULL +string(0) "" +string(0) "" string(30) "https://:password@example.com/" diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 954753142885b..e8666a160f338 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -274,11 +274,17 @@ static zend_result php_uri_parser_whatwg_scheme_write(void *uri, zval *value, zv return SUCCESS; } +/* 4.2. URL miscellaneous: A URL includes credentials if its username or password is not the empty string. */ +static bool includes_credentials(const lxb_url_t *lexbor_uri) +{ + return lexbor_uri->username.length > 0 || lexbor_uri->password.length > 0; +} + static zend_result php_uri_parser_whatwg_username_read(void *uri, php_uri_component_read_mode read_mode, zval *retval) { const lxb_url_t *lexbor_uri = uri; - if (lexbor_uri->username.length) { + if (includes_credentials(lexbor_uri)) { ZVAL_STRINGL(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length); } else { ZVAL_NULL(retval); @@ -307,7 +313,7 @@ static zend_result php_uri_parser_whatwg_password_read(void *uri, php_uri_compon { const lxb_url_t *lexbor_uri = uri; - if (lexbor_uri->password.length > 0) { + if (includes_credentials(lexbor_uri)) { ZVAL_STRINGL(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length); } else { ZVAL_NULL(retval); From 35d583443c6b58999978e4c827188be9a8498e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sat, 1 Nov 2025 13:25:51 +0100 Subject: [PATCH 2/3] uri: Use ZVAL_STRINGL_FAST in `whatwg_(username|password)_read()` This nicely sidesteps the undefined behavior with passing a `(NULL, 0)` pair without needing manual logic. --- ext/uri/uri_parser_whatwg.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index e8666a160f338..ef9bf0e020c34 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -285,7 +285,7 @@ static zend_result php_uri_parser_whatwg_username_read(void *uri, php_uri_compon const lxb_url_t *lexbor_uri = uri; if (includes_credentials(lexbor_uri)) { - ZVAL_STRINGL(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length); + ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length); } else { ZVAL_NULL(retval); } @@ -314,7 +314,7 @@ static zend_result php_uri_parser_whatwg_password_read(void *uri, php_uri_compon const lxb_url_t *lexbor_uri = uri; if (includes_credentials(lexbor_uri)) { - ZVAL_STRINGL(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length); + ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length); } else { ZVAL_NULL(retval); } From 4bb984a7b060e1888dccbaa0afca45a191d829cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sat, 1 Nov 2025 13:29:40 +0100 Subject: [PATCH 3/3] NEWS --- NEWS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS b/NEWS index 66f822bbd2fe7..ba12a18817c2c 100644 --- a/NEWS +++ b/NEWS @@ -28,6 +28,11 @@ PHP NEWS . Fixed bug GH-19798: XP_SOCKET XP_SSL (Socket stream modules): Incorrect condition for Win32/Win64. (Jakub Zelenka) +- URI: + . Use the "includes credentials" rule of the WHATWG URL Standard to + decide whether Uri\WhatWg\Url::getUsername() and ::getPassword() + getters should return null or an empty string. (timwolla) + - Zip: . Fixed missing zend_release_fcall_info_cache on the following methods ZipArchive::registerProgressCallback() and ZipArchive::registerCancelCallback()