Commit 92b08946a33 for php.net

commit 92b08946a33e00c7b272acaa7a1c0240cd16edc6
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sun Sep 6 10:56:24 2026 -0400

    dom: fix attribute namespaces from foreign content in the HTML parser

    The bridge registered an id as XML_ATTRIBUTE_ID only when lexbor
    reported the HTML namespace, so ids on SVG and MathML elements never
    reached getElementById(). Separately, lexbor initializes every attribute
    node with its element's namespace and overrides that only through the
    foreign-attribute adjust table, which the parser wires up for SVG and
    MathML alone, so a fragment parsed with an xlink, xml or xmlns context
    element came back as <z xlink:id="q" xlink:foo="1">. Restrict the three
    namespace branches to attributes lexbor adjusted, and key the id
    registration off the namespace the bridge assigned.

    Closes GH-23598

diff --git a/NEWS b/NEWS
index 50815738590..69b3dec86ed 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,11 @@ PHP                                                                        NEWS
     (Ilia Alshanetsky)
   . Fixed bug GH-23365 (DOMNode::insertBefore($n, $n) drops the node and
     leaves a self-referencing sibling list). (David Carlier)
+  . Fixed Dom\HTMLDocument::getElementById() not finding ids of SVG and
+    MathML elements. (Ilia Alshanetsky)
+  . Fixed Dom\HTMLDocument giving attributes the namespace of their element
+    when a fragment is parsed with an xlink, xml or xmlns context element.
+    (Ilia Alshanetsky)

 - Intl:
   . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state
diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c
index 34320a122f5..24f89cb0faf 100644
--- a/ext/dom/html5_parser.c
+++ b/ext/dom/html5_parser.c
@@ -241,7 +241,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert(
                 lxml_attr->children = lxml_attr->last = lxml_text;
                 lxml_text->parent = (xmlNodePtr) lxml_attr;

-                if (attr->node.ns == LXB_NS_XMLNS) {
+                if (attr->node.ns == LXB_NS_XMLNS && (attr->node.prefix || strcmp((const char *) local_name, "xmlns") == 0)) {
                     if (strcmp((const char *) local_name, "xmlns") != 0) {
                         if (prefixed_xmlns_ns == NULL) {
                             prefixed_xmlns_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xmlns", DOM_XMLNS_NS_URI);
@@ -251,13 +251,13 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert(
                         lxml_attr->ns = php_dom_libxml_ns_mapper_ensure_prefixless_xmlns_ns(ns_mapper);
                     }
                     lxml_attr->ns->_private = (void *) php_dom_ns_is_xmlns_magic_token;
-                } else if (attr->node.ns == LXB_NS_XLINK) {
+                } else if (attr->node.prefix && attr->node.ns == LXB_NS_XLINK) {
                     if (xlink_ns == NULL) {
                         xlink_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xlink", DOM_XLINK_NS_URI);
                         xlink_ns->_private = (void *) php_dom_ns_is_xlink_magic_token;
                     }
                     lxml_attr->ns = xlink_ns;
-                } else if (attr->node.ns == LXB_NS_XML) {
+                } else if (attr->node.prefix && attr->node.ns == LXB_NS_XML) {
                     if (xml_ns == NULL) {
                         xml_ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, "xml", DOM_XML_NS_URI);
                         xml_ns->_private = (void *) php_dom_ns_is_xml_magic_token;
@@ -274,7 +274,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert(
                 last_added_attr = lxml_attr;

                 /* xmlIsID does some other stuff too that is irrelevant here. */
-                if (local_name_length == 2 && local_name[0] == 'i' && local_name[1] == 'd' && attr->node.ns == LXB_NS_HTML) {
+                if (local_name_length == 2 && local_name[0] == 'i' && local_name[1] == 'd' && lxml_attr->ns == NULL) {
                     if (xmlAddID(NULL, lxml_doc, value, lxml_attr) == 0) {
                         /* If the ID already exists, the ID attribute still needs to be marked as an ID. */
                         lxml_attr->atype = XML_ATTRIBUTE_ID;
diff --git a/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt b/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt
new file mode 100644
index 00000000000..ba6d33fdbbc
--- /dev/null
+++ b/ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt
@@ -0,0 +1,59 @@
+--TEST--
+Unprefixed attributes in foreign content are in no namespace
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$html = '<!DOCTYPE html><html><body><svg id="s" xlink:href="#a" xml:lang="en" xmlns:xlink="urn:x"><rect xml:id="r"/></svg><math id="m"></math><p id="p"></p></body></html>';
+$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
+
+var_dump([
+    'svg #s' => $doc->getElementById('s')?->tagName,
+    'math #m' => $doc->getElementById('m')?->tagName,
+    'html #p' => $doc->getElementById('p')?->tagName,
+    'xml:id #r' => $doc->getElementById('r')?->tagName,
+]);
+
+foreach ($doc->getElementById('s')->attributes as $attr) {
+    echo $attr->name, ' => ', var_export($attr->namespaceURI, true), "\n";
+}
+
+$contexts = [
+    'http://www.w3.org/1999/xlink' => 'z',
+    'http://www.w3.org/XML/1998/namespace' => 'z',
+    'http://www.w3.org/2000/xmlns/' => 'xmlns',
+];
+foreach ($contexts as $uri => $name) {
+    $fragment_doc = Dom\HTMLDocument::createEmpty();
+    $context = $fragment_doc->createElementNS($uri, $name);
+    $fragment_doc->appendChild($context);
+    $context->innerHTML = '<z id="q" xlink:href="#a"></z>';
+
+    echo $uri, "\n  ", $context->innerHTML, "\n  ";
+    var_dump($fragment_doc->getElementById('q')?->tagName);
+}
+?>
+--EXPECT--
+array(4) {
+  ["svg #s"]=>
+  string(3) "svg"
+  ["math #m"]=>
+  string(4) "math"
+  ["html #p"]=>
+  string(1) "P"
+  ["xml:id #r"]=>
+  NULL
+}
+id => NULL
+xlink:href => 'http://www.w3.org/1999/xlink'
+xml:lang => 'http://www.w3.org/XML/1998/namespace'
+xmlns:xlink => 'http://www.w3.org/2000/xmlns/'
+http://www.w3.org/1999/xlink
+  <z id="q" xlink:href="#a"></z>
+  string(1) "Z"
+http://www.w3.org/XML/1998/namespace
+  <z id="q" xlink:href="#a"></z>
+  string(1) "Z"
+http://www.w3.org/2000/xmlns/
+  <z id="q" xlink:href="#a"></z>
+  string(1) "Z"