From 81f6ba550325a6fd513217adb67d99479f224944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sat, 1 Nov 2025 14:15:06 +0100 Subject: [PATCH] =?UTF-8?q?uri:=20Use=20the=20=E2=80=9Cincludes=20credenti?= =?UTF-8?q?als=E2=80=9D=20rule=20for=20WhatWg=20user/password=20getters=20?= =?UTF-8?q?(#20303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * uri: Use the “includes credentials” rule for WhatWg user/password getters 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). * 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. * NEWS --- NEWS | 5 +++++ .../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 | 14 ++++++++++---- 8 files changed, 59 insertions(+), 10 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/NEWS b/NEWS index 66f822bbd2fe..ba12a18817c2 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() 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 957973b062a2..203b831411a6 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 000000000000..06bade29468f --- /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 a2140669ee8a..a795f3197a6d 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 f71deff3f207..8340b4ca1db6 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 000000000000..ccfeac222e4e --- /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 e5af4efb223d..7886b35fd9b8 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 954753142885..ef9bf0e020c3 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -274,12 +274,18 @@ 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) { - ZVAL_STRINGL(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length); + if (includes_credentials(lexbor_uri)) { + ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->username.data, lexbor_uri->username.length); } else { ZVAL_NULL(retval); } @@ -307,8 +313,8 @@ 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) { - ZVAL_STRINGL(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length); + if (includes_credentials(lexbor_uri)) { + ZVAL_STRINGL_FAST(retval, (const char *) lexbor_uri->password.data, lexbor_uri->password.length); } else { ZVAL_NULL(retval); }