Commit 589b96c14bd for php.net
commit 589b96c14bd8ea7a907db37634984c690f73b558
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date: Sat Jun 27 23:11:59 2026 +0200
Apply lexbor fix for empty hosts (#22485)
Fixed behavior of Uri\WhatWg\Url wither methods with regards to empty opaque hosts (e.g. "scheme://") by applying the https://github.com/lexbor/lexbor/commit/cf07699ca0f5fa4e1f7fd05c2135fd38e6d196c2 upstream fix for lexbor.
diff --git a/NEWS b/NEWS
index 4022869c0b0..9522c202883 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,10 @@ PHP NEWS
. Fixed bug GH-22395 (base_convert() outputs at most 64 characters).
(Weilin Du)
+- URI:
+ . Fixed behavior of Uri\WhatWg\Url wither methods with regards to empty
+ opaque hosts. (kocsismate)
+
02 Jul 2026, PHP 8.5.8
- Core:
diff --git a/ext/lexbor/lexbor/url/url.c b/ext/lexbor/lexbor/url/url.c
index 5a1143469d1..86bcf8f3502 100644
--- a/ext/lexbor/lexbor/url/url.c
+++ b/ext/lexbor/lexbor/url/url.c
@@ -1115,11 +1115,13 @@ lxb_url_host_copy(const lxb_url_host_t *src, lxb_url_host_t *dst,
dst->type = src->type;
- if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
- if (src->type == LXB_URL_HOST_TYPE__UNDEF) {
- return LXB_STATUS_OK;
- }
+ if (src->type == LXB_URL_HOST_TYPE__UNDEF
+ || src->type == LXB_URL_HOST_TYPE_EMPTY)
+ {
+ return LXB_STATUS_OK;
+ }
+ if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
return lxb_url_str_copy(&src->u.domain,
&dst->u.domain, dst_mraw);
}
@@ -1152,6 +1154,24 @@ lxb_url_host_set_empty(lxb_url_host_t *host, lexbor_mraw_t *mraw)
host->type = LXB_URL_HOST_TYPE_EMPTY;
}
+lxb_inline bool
+lxb_url_host_is_empty(const lxb_url_host_t *host)
+{
+ if (host->type == LXB_URL_HOST_TYPE_EMPTY) {
+ return true;
+ }
+
+ if (host->type == LXB_URL_HOST_TYPE_DOMAIN) {
+ return host->u.domain.length == 0;
+ }
+
+ if (host->type == LXB_URL_HOST_TYPE_OPAQUE) {
+ return host->u.opaque.length == 0;
+ }
+
+ return false;
+}
+
static bool
lxb_url_host_eq(lxb_url_host_t *host, const lxb_char_t *data, size_t length)
{
@@ -1251,7 +1271,7 @@ lxb_url_normalized_windows_drive_letter(const lxb_char_t *data,
static bool
lxb_url_cannot_have_user_pass_port(lxb_url_t *url)
{
- return url->host.type == LXB_URL_HOST_TYPE_EMPTY
+ return lxb_url_host_is_empty(&url->host)
|| url->host.type == LXB_URL_HOST_TYPE__UNDEF
|| url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE;
}
@@ -3978,6 +3998,11 @@ lxb_url_opaque_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
lxb_status_t status;
const lxb_char_t *p;
+ if (data == end) {
+ lxb_url_host_set_empty(host, mraw);
+ return LXB_STATUS_OK;
+ }
+
p = data;
while (p < end) {
diff --git a/ext/lexbor/patches/0007-Fix-empty-port-setter.patch b/ext/lexbor/patches/0007-Fix-empty-port-setter.patch
new file mode 100644
index 00000000000..75ceaab0c63
--- /dev/null
+++ b/ext/lexbor/patches/0007-Fix-empty-port-setter.patch
@@ -0,0 +1,191 @@
+From cf07699ca0f5fa4e1f7fd05c2135fd38e6d196c2 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov <lex.borisov@gmail.com>
+Date: Fri, 26 Jun 2026 18:55:56 +0300
+Subject: [PATCH] URL: fixed setters for empty hosts.
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Empty non-special hosts were represented as empty opaque hosts, so
+lxb_url_cannot_have_user_pass_port() allowed username, password, and port
+setters to modify scheme://.
+
+For fixed this store empty opaque-host input as LXB_URL_HOST_TYPE_EMPTY.
+
+Per report from Máté Kocsis (@kocsismate).
+
+This relates to #387 issue on GitHub.
+---
+ source/lexbor/url/url.c | 35 ++++++++++++++++++---
+ test/files/lexbor/url/changes.ton | 52 +++++++++++++++++++++++++++++--
+ test/files/lexbor/url/url.ton | 8 ++++-
+ 3 files changed, 86 insertions(+), 9 deletions(-)
+
+diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
+index ced4462b..e1da2c38 100644
+--- a/source/lexbor/url/url.c
++++ b/source/lexbor/url/url.c
+@@ -1116,11 +1116,13 @@ lxb_url_host_copy(const lxb_url_host_t *src, lxb_url_host_t *dst,
+
+ dst->type = src->type;
+
+- if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
+- if (src->type == LXB_URL_HOST_TYPE__UNDEF) {
+- return LXB_STATUS_OK;
+- }
++ if (src->type == LXB_URL_HOST_TYPE__UNDEF
++ || src->type == LXB_URL_HOST_TYPE_EMPTY)
++ {
++ return LXB_STATUS_OK;
++ }
+
++ if (src->type <= LXB_URL_HOST_TYPE_OPAQUE) {
+ return lxb_url_str_copy(&src->u.domain,
+ &dst->u.domain, dst_mraw);
+ }
+@@ -1153,6 +1155,24 @@ lxb_url_host_set_empty(lxb_url_host_t *host, lexbor_mraw_t *mraw)
+ host->type = LXB_URL_HOST_TYPE_EMPTY;
+ }
+
++lxb_inline bool
++lxb_url_host_is_empty(const lxb_url_host_t *host)
++{
++ if (host->type == LXB_URL_HOST_TYPE_EMPTY) {
++ return true;
++ }
++
++ if (host->type == LXB_URL_HOST_TYPE_DOMAIN) {
++ return host->u.domain.length == 0;
++ }
++
++ if (host->type == LXB_URL_HOST_TYPE_OPAQUE) {
++ return host->u.opaque.length == 0;
++ }
++
++ return false;
++}
++
+ static bool
+ lxb_url_host_eq(lxb_url_host_t *host, const lxb_char_t *data, size_t length)
+ {
+@@ -1252,7 +1272,7 @@ lxb_url_normalized_windows_drive_letter(const lxb_char_t *data,
+ static bool
+ lxb_url_cannot_have_user_pass_port(lxb_url_t *url)
+ {
+- return url->host.type == LXB_URL_HOST_TYPE_EMPTY
++ return lxb_url_host_is_empty(&url->host)
+ || url->host.type == LXB_URL_HOST_TYPE__UNDEF
+ || url->scheme.type == LXB_URL_SCHEMEL_TYPE_FILE;
+ }
+@@ -3979,6 +3999,11 @@ lxb_url_opaque_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
+ lxb_status_t status;
+ const lxb_char_t *p;
+
++ if (data == end) {
++ lxb_url_host_set_empty(host, mraw);
++ return LXB_STATUS_OK;
++ }
++
+ p = data;
+
+ while (p < end) {
+diff --git a/test/files/lexbor/url/changes.ton b/test/files/lexbor/url/changes.ton
+index 07bc9449..1a0b6e35 100644
+--- a/test/files/lexbor/url/changes.ton
++++ b/test/files/lexbor/url/changes.ton
+@@ -1,5 +1,5 @@
+ [
+- /* Test count: 1 */
++ /* Test count: 47 */
+ /* 1 */
+ {
+ "url": "https://user:pass@lexbor.com/docs/html/path?x=y&a=b#best-fragment",
+@@ -982,9 +982,53 @@
+ "failed": false
+ },
+ /* 45 */
++ {
++ "url": "scheme://",
++ "done": "scheme://",
++ "change": {
++ "href": null,
++ "protocol": null,
++ "username": "user",
++ "password": "pass",
++ "host": null,
++ "hostname": null,
++ "port": "433",
++ "pathname": null,
++ "search": null,
++ "hash": null
++ },
++ "scheme": "scheme",
++ "host": "",
++ "path": "",
++ "failed": false
++ },
++ /* 46 */
++ {
++ "url": "scheme://host",
++ "done": "scheme://host:433",
++ "change": {
++ "href": null,
++ "protocol": null,
++ "username": null,
++ "password": null,
++ "host": null,
++ "hostname": null,
++ "port": "433",
++ "pathname": null,
++ "search": null,
++ "hash": null
++ },
++ "scheme": "scheme",
++ "host": "host",
++ "port": 433,
++ "has_port": true,
++ "path": "",
++ "failed": false
++ },
++ /* 47 */
+ {
+ "url": "https://example.com:432",
+- "done": "https://example.com:432",
++ "done": "https://example.com:432/",
+ "change": {
+ "href": null,
+ "protocol": null,
+@@ -999,7 +1043,9 @@
+ },
+ "scheme": "https",
+ "host": "example.com",
+- "port": "432",
++ "port": 432,
++ "has_port": true,
++ "path": "/",
+ "failed": true
+ }
+ ]
+diff --git a/test/files/lexbor/url/url.ton b/test/files/lexbor/url/url.ton
+index 2baa4bc2..85794c5b 100644
+--- a/test/files/lexbor/url/url.ton
++++ b/test/files/lexbor/url/url.ton
+@@ -1,5 +1,5 @@
+ [
+- /* Test count: 7 */
++ /* Test count: 8 */
+ /* 1 */
+ {
+ "url": "https://user:pass@lexbor.com:450/docs/lexbor/?search=lxb_status_t#version",
+@@ -74,5 +74,11 @@
+ "path": "",
+ "failed": false,
+ "encoding": "utf-8"
++ },
++ /* 8 */
++ {
++ "url": "scheme://:433",
++ "failed": true,
++ "encoding": "utf-8"
+ }
+ ]
diff --git a/ext/uri/tests/whatwg/modification/password_success_empty_host.phpt b/ext/uri/tests/whatwg/modification/password_success_empty_host.phpt
new file mode 100644
index 00000000000..217e8f174d8
--- /dev/null
+++ b/ext/uri/tests/whatwg/modification/password_success_empty_host.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\Url component modification - password - empty host
+--FILE--
+<?php
+
+$url1 = new Uri\WhatWg\Url("scheme://");
+$url2 = $url1->withPassword("password");
+
+var_dump($url1->getPassword());
+var_dump($url2->getPassword());
+var_dump($url2->toAsciiString());
+
+?>
+--EXPECT--
+NULL
+NULL
+string(9) "scheme://"
diff --git a/ext/uri/tests/whatwg/modification/port_success_empty_host.phpt b/ext/uri/tests/whatwg/modification/port_success_empty_host.phpt
new file mode 100644
index 00000000000..df392ab4b74
--- /dev/null
+++ b/ext/uri/tests/whatwg/modification/port_success_empty_host.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\Url component modification - port - empty host
+--FILE--
+<?php
+
+$url1 = new Uri\WhatWg\Url("scheme://");
+$url2 = $url1->withPort(433);
+
+var_dump($url1->getPort());
+var_dump($url2->getPort());
+var_dump($url2->toAsciiString());
+
+?>
+--EXPECT--
+NULL
+NULL
+string(9) "scheme://"
diff --git a/ext/uri/tests/whatwg/modification/username_success_empty_host.phpt b/ext/uri/tests/whatwg/modification/username_success_empty_host.phpt
new file mode 100644
index 00000000000..13357d269b4
--- /dev/null
+++ b/ext/uri/tests/whatwg/modification/username_success_empty_host.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\Url component modification - username - empty host
+--FILE--
+<?php
+
+$url1 = new Uri\WhatWg\Url("scheme://");
+$url2 = $url1->withUsername("user");
+
+var_dump($url1->getUsername());
+var_dump($url2->getUsername());
+var_dump($url2->toAsciiString());
+
+?>
+--EXPECT--
+NULL
+NULL
+string(9) "scheme://"