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://"