From d46dc5694c3cd4c1492449941bb34c050efea751 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 23 Jul 2023 17:10:55 +0200 Subject: [PATCH] Fix various namespace prefix conflict resolution bugs and namespace shift bugs There are two linked issues: - Conflicts couldn't be resolved by changing the prefix name. - Lacking a prefix would shift the namespace as the default namespace, causing elements to suddenly become part of the namespace instead of the attributes. The output could still be improved by removing redundant namespace declarations, but that's another issue. At least the output is correct now. Closes GH-11777. --- NEWS | 3 + UPGRADING | 6 ++ ext/dom/document.c | 6 +- ext/dom/element.c | 58 ++--------------- ext/dom/php_dom.c | 59 +++++++++++------ ext/dom/php_dom.h | 1 + ..._importNode_attribute_prefix_conflict.phpt | 65 +++++++++++++++++++ ...lement_setAttributeNS_prefix_conflict.phpt | 35 ++++++++++ .../setAttributeNS_with_prefix.phpt | 36 ++++++++++ .../setAttributeNS_without_prefix.phpt | 36 ++++++++++ .../setAttribute_mixed_prefix.phpt | 20 ++++++ .../setAttribute_with_prefix.phpt | 36 ++++++++++ .../setAttribute_without_prefix.phpt | 36 ++++++++++ ...mespace_definition_crash_in_attribute.phpt | 6 +- 14 files changed, 323 insertions(+), 80 deletions(-) create mode 100644 ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt create mode 100644 ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt create mode 100644 ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt create mode 100644 ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt create mode 100644 ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt create mode 100644 ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt create mode 100644 ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt diff --git a/NEWS b/NEWS index 0ad6a06adc0..75bfad53c8d 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,9 @@ PHP NEWS . Align DOMChildNode parent checks with spec. (nielsdos) . Fixed bug #80927 (Removing documentElement after creating attribute node: possible use-after-free). (nielsdos) + . Fix various namespace prefix conflict resolution bugs. (nielsdos) + . Fix calling createAttributeNS() without prefix causing the default + namespace of the element to change. (nielsdos) - Opcache: . Avoid resetting JIT counter handlers from multiple processes/threads. diff --git a/UPGRADING b/UPGRADING index 95f7a24ebc7..3a33f42af07 100644 --- a/UPGRADING +++ b/UPGRADING @@ -53,6 +53,12 @@ PHP 8.3 UPGRADE NOTES . Using the DOMParentNode and DOMChildNode methods without a document now works instead of throwing a HIERARCHY_REQUEST_ERR DOMException. This is in line with the behaviour spec demands. + . createAttributeNS() without specifying a prefix would incorrectly create a default + namespace, placing the element inside the namespace instead of the attribute. + This bug is now fixed. + . createAttributeNS() would previously incorrectly throw a NAMESPACE_ERR when the + prefix was already used for a different uri. It now correctly chooses a + different prefix when there's a prefix name conflict. - FFI: . C functions that have a return type of void now return null instead of diff --git a/ext/dom/document.c b/ext/dom/document.c index bfb06d7e4cc..f254d87e7d6 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -805,7 +805,7 @@ PHP_METHOD(DOMDocument, importNode) xmlNodePtr root = xmlDocGetRootElement(docp); nsptr = xmlSearchNsByHref (nodep->doc, root, nodep->ns->href); - if (nsptr == NULL) { + if (nsptr == NULL || nsptr->prefix == NULL) { int errorcode; nsptr = dom_get_ns(root, (char *) nodep->ns->href, &errorcode, (char *) nodep->ns->prefix); } @@ -910,8 +910,8 @@ PHP_METHOD(DOMDocument, createAttributeNS) nodep = (xmlNodePtr) xmlNewDocProp(docp, (xmlChar *) localname, NULL); if (nodep != NULL && uri_len > 0) { nsptr = xmlSearchNsByHref(nodep->doc, root, (xmlChar *) uri); - if (nsptr == NULL) { - nsptr = dom_get_ns(root, uri, &errorcode, prefix); + if (nsptr == NULL || nsptr->prefix == NULL) { + nsptr = dom_get_ns(root, uri, &errorcode, prefix ? prefix : "default"); } xmlSetNs(nodep, nsptr); } diff --git a/ext/dom/element.c b/ext/dom/element.c index 8fb19d9e2ea..fabc3c514bf 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -655,45 +655,6 @@ PHP_METHOD(DOMElement, getAttributeNS) } /* }}} end dom_element_get_attribute_ns */ -static xmlNsPtr _dom_new_reconNs(xmlDocPtr doc, xmlNodePtr tree, xmlNsPtr ns) /* {{{ */ -{ - xmlNsPtr def; - xmlChar prefix[50]; - int counter = 1; - - if ((tree == NULL) || (ns == NULL) || (ns->type != XML_NAMESPACE_DECL)) { - return NULL; - } - - /* Code taken from libxml2 (2.6.20) xmlNewReconciliedNs - * - * Find a close prefix which is not already in use. - * Let's strip namespace prefixes longer than 20 chars ! - */ - if (ns->prefix == NULL) - snprintf((char *) prefix, sizeof(prefix), "default"); - else - snprintf((char *) prefix, sizeof(prefix), "%.20s", (char *)ns->prefix); - - def = xmlSearchNs(doc, tree, prefix); - while (def != NULL) { - if (counter > 1000) return(NULL); - if (ns->prefix == NULL) - snprintf((char *) prefix, sizeof(prefix), "default%d", counter++); - else - snprintf((char *) prefix, sizeof(prefix), "%.20s%d", - (char *)ns->prefix, counter++); - def = xmlSearchNs(doc, tree, prefix); - } - - /* - * OK, now we are ready to create a new one. - */ - def = xmlNewNs(tree, ns->href, prefix); - return(def); -} -/* }}} */ - /* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#core-ID-ElSetAttrNS Since: DOM Level 2 */ @@ -756,27 +717,18 @@ PHP_METHOD(DOMElement, setAttributeNS) tmpnsptr = tmpnsptr->next; } if (tmpnsptr == NULL) { - nsptr = _dom_new_reconNs(elemp->doc, elemp, nsptr); + nsptr = dom_get_ns_resolve_prefix_conflict(elemp, (const char *) nsptr->href); } } } if (nsptr == NULL) { - if (prefix == NULL) { - if (is_xmlns == 1) { - xmlNewNs(elemp, (xmlChar *)value, NULL); - xmlReconciliateNs(elemp->doc, elemp); - } else { - errorcode = NAMESPACE_ERR; - } + if (is_xmlns == 1) { + xmlNewNs(elemp, (xmlChar *)value, prefix == NULL ? NULL : (xmlChar *)localname); } else { - if (is_xmlns == 1) { - xmlNewNs(elemp, (xmlChar *)value, (xmlChar *)localname); - } else { - nsptr = dom_get_ns(elemp, uri, &errorcode, prefix); - } - xmlReconciliateNs(elemp->doc, elemp); + nsptr = dom_get_ns(elemp, uri, &errorcode, prefix); } + xmlReconciliateNs(elemp->doc, elemp); } else { if (is_xmlns == 1) { if (nsptr->href) { diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 9e036214bde..bada817e301 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -32,10 +32,6 @@ #define PHP_XPATH 1 #define PHP_XPTR 2 -/* libxml2 doesn't expose this constant as part of their public API. - * See xmlDOMReconcileNSOptions in tree.c */ -#define PHP_LIBXML2_DOM_RECONNS_REMOVEREDUND (1 << 0) - /* {{{ class entries */ PHP_DOM_EXPORT zend_class_entry *dom_node_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_domexception_class_entry; @@ -1473,8 +1469,7 @@ static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep * Although libxml2 currently does not use this for the reconciliation, it still * makes sense to do this just in case libxml2's internal change in the future. */ xmlDOMWrapCtxt dummy_ctxt = {0}; - bool remove_redundant = nodep->nsDef == NULL && nodep->ns != NULL; - xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ remove_redundant ? PHP_LIBXML2_DOM_RECONNS_REMOVEREDUND : 0); + xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ 0); } void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */ @@ -1557,6 +1552,35 @@ int dom_check_qname(char *qname, char **localname, char **prefix, int uri_len, i } /* }}} */ +/* Creates a new namespace declaration with a random prefix with the given uri on the tree. + * This is used to resolve a namespace prefix conflict in cases where spec does not want a + * namespace error in case of conflicts, but demands a resolution. */ +xmlNsPtr dom_get_ns_resolve_prefix_conflict(xmlNodePtr tree, const char *uri) +{ + ZEND_ASSERT(tree != NULL); + xmlDocPtr doc = tree->doc; + + if (UNEXPECTED(doc == NULL)) { + return NULL; + } + + /* Code adapted from libxml2 (2.10.4) */ + char prefix[50]; + int counter = 1; + snprintf(prefix, sizeof(prefix), "default"); + xmlNsPtr nsptr = xmlSearchNs(doc, tree, (const xmlChar *) prefix); + while (nsptr != NULL) { + if (counter > 1000) { + return NULL; + } + snprintf(prefix, sizeof(prefix), "default%d", counter++); + nsptr = xmlSearchNs(doc, tree, (const xmlChar *) prefix); + } + + /* Search yielded no conflict */ + return xmlNewNs(tree, (const xmlChar *) uri, (const xmlChar *) prefix); +} + /* http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-DocCrElNS @@ -1574,28 +1598,21 @@ xmlNsPtr dom_get_ns(xmlNodePtr nodep, char *uri, int *errorcode, char *prefix) { if (! ((prefix && !strcmp (prefix, "xml") && strcmp(uri, (char *)XML_XML_NAMESPACE)) || (prefix && !strcmp (prefix, "xmlns") && strcmp(uri, (char *)DOM_XMLNS_NAMESPACE)) || (prefix && !strcmp(uri, (char *)DOM_XMLNS_NAMESPACE) && strcmp (prefix, "xmlns")))) { - /* Reuse the old namespaces from doc->oldNs if possible, before creating a new one. - * This will prevent the oldNs list from growing with duplicates. */ - xmlDocPtr doc = nodep->doc; - if (doc && doc->oldNs != NULL) { - nsptr = doc->oldNs; - do { - if (xmlStrEqual(nsptr->prefix, (xmlChar *)prefix) && xmlStrEqual(nsptr->href, (xmlChar *)uri)) { - goto out; - } - nsptr = nsptr->next; - } while (nsptr); - } - /* Couldn't reuse one, create a new one. */ nsptr = xmlNewNs(nodep, (xmlChar *)uri, (xmlChar *)prefix); if (UNEXPECTED(nsptr == NULL)) { - goto err; + /* Either memory allocation failure, or it's because of a prefix conflict. + * We'll assume a conflict and try again. If it was a memory allocation failure we'll just fail again, whatever. + * This isn't needed for every caller (such as createElementNS & DOMElement::__construct), but isn't harmful and simplifies the mental model "when do I use which function?". + * This branch will also be taken unlikely anyway as in those cases it'll be for allocation failure. */ + nsptr = dom_get_ns_resolve_prefix_conflict(nodep, uri); + if (UNEXPECTED(nsptr == NULL)) { + goto err; + } } } else { goto err; } -out: *errorcode = 0; return nsptr; err: diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index b4a6a2d01e8..4d415256788 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -151,6 +151,7 @@ zend_string *dom_node_concatenated_name_helper(size_t name_len, const char *name zend_string *dom_node_get_node_name_attribute_or_element(const xmlNode *nodep); bool php_dom_is_node_connected(const xmlNode *node); bool php_dom_adopt_node(xmlNodePtr nodep, dom_object *dom_object_new_document, xmlDocPtr new_document); +xmlNsPtr dom_get_ns_resolve_prefix_conflict(xmlNodePtr tree, const char *uri); /* parentnode */ void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc); diff --git a/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt b/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt new file mode 100644 index 00000000000..d00fedbfb3d --- /dev/null +++ b/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt @@ -0,0 +1,65 @@ +--TEST-- +DOMDocument::importNode() with attribute prefix name conflict +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('foo:bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +echo "--- Non-default namespace test case with a default namespace in the destination ---\n"; + +$dom1 = new DOMDocument(); +$dom2 = new DOMDocument(); +$dom1->loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('foo:bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +echo "--- Default namespace test case ---\n"; + +// We don't expect the namespace to be imported because default namespaces on the same element don't apply to attributes +// but the attribute should be imported +$dom1 = new DOMDocument(); +$dom2 = new DOMDocument(); +$dom1->loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +?> +--EXPECT-- +--- Non-default namespace test case without a default namespace in the destination --- + + + + +--- Non-default namespace test case with a default namespace in the destination --- + + + + +--- Default namespace test case --- + + + + diff --git a/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt b/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt new file mode 100644 index 00000000000..fc491b59ea3 --- /dev/null +++ b/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt @@ -0,0 +1,35 @@ +--TEST-- +DOMElement::setAttributeNS() with prefix name conflict +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$dom->documentElement->setAttributeNS('http://php.net/2', 'foo:bar', 'no1'); +echo $dom->saveXML(); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no2'); +echo $dom->saveXML(); + +echo "--- Default namespace test case ---\n"; + +$dom = new DOMDocument(); +$dom->loadXML(''); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no1'); +echo $dom->saveXML(); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no2'); +echo $dom->saveXML(); +?> +--EXPECT-- +--- Non-default namespace test case --- + + + + +--- Default namespace test case --- + + + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt new file mode 100644 index 00000000000..1ca679d576d --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNodeNS variation, with prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns2', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns3', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns4', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +NULL + + + +NULL + + + +NULL + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt new file mode 100644 index 00000000000..6ab6c2292a7 --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNodeNS variation, without prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns2', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns3', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns4', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +NULL + + + +NULL + + + +NULL + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt new file mode 100644 index 00000000000..a53d7304dc8 --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt @@ -0,0 +1,20 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), mixed +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL +string(18) "http://php.net/ns1" + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt new file mode 100644 index 00000000000..161eadd353b --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), with prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns2', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns3', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns4', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +string(18) "http://php.net/ns1" + + + +string(18) "http://php.net/ns2" + + + +string(18) "http://php.net/ns3" + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt new file mode 100644 index 00000000000..34717ff6ab5 --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), without prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns2', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns3', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns4', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +string(18) "http://php.net/ns1" + + + +string(18) "http://php.net/ns2" + + + +string(18) "http://php.net/ns3" + + diff --git a/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt b/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt index 64f2fbfa007..b43f7c34bae 100644 --- a/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt +++ b/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt @@ -39,9 +39,9 @@ echo $doc->saveXML($attr3->parentNode), "\n"; ?> --EXPECT-- - + - + string(15) "hello content 2" string(0) "" string(8) "some:ns2" @@ -49,5 +49,5 @@ NULL string(0) "" string(7) "some:ns" string(7) "some:ns" - hello="" + default1:hello=""