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"