From 6761e4fcb4d594254bcd7697373f678dcd98aae3 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 6 Sep 2026 10:56:24 -0400 Subject: [PATCH] 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 . Restrict the three namespace branches to attributes lexbor adjusted, and key the id registration off the namespace the bridge assigned. Closes GH-23598 --- NEWS | 5 ++ ext/dom/html5_parser.c | 8 +-- ...refixed_attributes_in_foreign_content.phpt | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 ext/dom/tests/modern/html/parser/HTMLDocument_unprefixed_attributes_in_foreign_content.phpt diff --git a/NEWS b/NEWS index 1cc265f02e8d..7fca384d23c6 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,11 @@ PHP NEWS that still have a live wrapper). (iliaal) . Fixed a use-after-free when Dom\Element::setAttributeNS() replaces the value of an attribute whose child still has a live wrapper. (iliaal) + . 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) - GD: . Fixed imageaffinematrixget() and imageaffinematrixconcat() reporting the diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c index 34320a122f53..24f89cb0fafb 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 000000000000..ba6d33fdbbc5 --- /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-- +

'; +$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 = ''; + + 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 + + string(1) "Z" +http://www.w3.org/XML/1998/namespace + + string(1) "Z" +http://www.w3.org/2000/xmlns/ + + string(1) "Z"