diff --git a/ext/dom/element.c b/ext/dom/element.c index cad154a51b161..b787e1bb4def8 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -640,6 +640,7 @@ PHP_METHOD(DOMElement, setAttributeNode) } xmlAddChild(nodep, (xmlNodePtr) attrp); + php_dom_reconcile_attribute_namespace_after_insertion(attrp); /* Returns old property if removed otherwise NULL */ if (existattrp != NULL) { @@ -1012,6 +1013,7 @@ PHP_METHOD(DOMElement, setAttributeNodeNS) } xmlAddChild(nodep, (xmlNodePtr) attrp); + php_dom_reconcile_attribute_namespace_after_insertion(attrp); /* Returns old property if removed otherwise NULL */ if (existattrp != NULL) { diff --git a/ext/dom/node.c b/ext/dom/node.c index 8417b0a9a13dc..5a4fdae1d3ff6 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -1259,6 +1259,7 @@ PHP_METHOD(DOMNode, appendChild) if (UNEXPECTED(new_child == NULL)) { goto cannot_add; } + php_dom_reconcile_attribute_namespace_after_insertion((xmlAttrPtr) new_child); } else if (child->type == XML_DOCUMENT_FRAG_NODE) { xmlNodePtr last = child->last; new_child = _php_dom_insert_fragment(nodep, nodep->last, NULL, child, intern, childobj); @@ -1362,6 +1363,13 @@ PHP_METHOD(DOMNode, cloneNode) } } + if (node->type == XML_ATTRIBUTE_NODE && n->ns != NULL && node->ns == NULL) { + /* Let reconciliation deal with this. The lifetime of the namespace poses no problem + * because we're increasing the refcount of the document proxy at the return. + * libxml2 doesn't set the ns because it can't know that this is safe. */ + node->ns = n->ns; + } + /* If document cloned we want a new document proxy */ if (node->doc != n->doc) { intern = NULL; diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index b83f628dc9416..ce540ad4a3b0a 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -1458,6 +1458,22 @@ static void dom_reconcile_ns_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePt } } +void php_dom_reconcile_attribute_namespace_after_insertion(xmlAttrPtr attrp) +{ + ZEND_ASSERT(attrp != NULL); + + if (attrp->ns != NULL) { + /* Try to link to an existing namespace. If that won't work, reconcile. */ + xmlNodePtr nodep = attrp->parent; + xmlNsPtr matching_ns = xmlSearchNs(nodep->doc, nodep, attrp->ns->prefix); + if (matching_ns && xmlStrEqual(matching_ns->href, attrp->ns->href)) { + attrp->ns = matching_ns; + } else { + xmlReconciliateNs(nodep->doc, nodep); + } + } +} + static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep) { /* Ideally we'd use the DOM-wrapped version, but we cannot: https://github.com/php/php-src/pull/12308. */ @@ -1474,6 +1490,8 @@ static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */ { + ZEND_ASSERT(nodep->type != XML_ATTRIBUTE_NODE); + /* Although the node type will be checked by the libxml2 API, * we still want to do the internal reconciliation conditionally. */ if (nodep->type == XML_ELEMENT_NODE) { diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index ceba2df06dc4e..b77036e83c294 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -152,6 +152,7 @@ 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); +void php_dom_reconcile_attribute_namespace_after_insertion(xmlAttrPtr attrp); /* 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 index d00fedbfb3d6c..d4a9ce5c34723 100644 --- a/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt +++ b/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt @@ -26,7 +26,9 @@ $dom1->loadXML('loadXML(''); $attribute = $dom1->documentElement->getAttributeNode('foo:bar'); $imported = $dom2->importNode($attribute); +var_dump($imported->prefix, $imported->namespaceURI); $dom2->documentElement->setAttributeNodeNS($imported); +var_dump($imported->prefix, $imported->namespaceURI); echo $dom1->saveXML(); echo $dom2->saveXML(); @@ -54,6 +56,10 @@ echo $dom2->saveXML(); --- Non-default namespace test case with a default namespace in the destination --- +string(7) "default" +string(14) "http://php.net" +string(7) "default" +string(14) "http://php.net" diff --git a/ext/dom/tests/clone_attribute_namespace_01.phpt b/ext/dom/tests/clone_attribute_namespace_01.phpt new file mode 100644 index 0000000000000..7870cbe82afd5 --- /dev/null +++ b/ext/dom/tests/clone_attribute_namespace_01.phpt @@ -0,0 +1,84 @@ +--TEST-- +Cloning an attribute should retain its namespace 01 +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); + $dom->documentElement->setAttributeNs("some:ns", "foo:bar", "value"); + + $attr = $dom->documentElement->getAttributeNodeNs("some:ns", "bar"); + $clone = $attr->cloneNode(true); + + return [$dom, $clone]; +} + +[$dom, $clone] = createTestDocument(); +var_dump($clone->prefix, $clone->namespaceURI); + +echo "--- Re-adding a namespaced attribute ---\n"; + +[$dom, $clone] = createTestDocument(); +$dom->documentElement->removeAttributeNs("some:ns", "bar"); +echo $dom->saveXML(); +$dom->documentElement->setAttributeNodeNs($clone); +echo $dom->saveXML(); + +echo "--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNodeNs variation) ---\n"; + +function readd_test(string $method) { + [$dom, $clone] = createTestDocument(); + $dom->documentElement->removeAttributeNs("some:ns", "bar"); + $dom->documentElement->removeAttribute("xmlns:foo"); + echo $dom->saveXML(); + $child = $dom->documentElement->appendChild($dom->createElement("child")); + $child->{$method}($clone); + echo $dom->saveXML(); +} + +readd_test("setAttributeNodeNs"); + +echo "--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNode variation) ---\n"; + +readd_test("setAttributeNode"); + +echo "--- Re-adding a namespaced attribute, with the namespace deleted (appendChild variation) ---\n"; + +readd_test("appendChild"); + +echo "--- Removing the document reference should not crash ---\n"; + +[$dom, $clone] = createTestDocument(); +unset($dom); +var_dump($clone->prefix, $clone->namespaceURI); + +?> +--EXPECT-- +string(3) "foo" +string(7) "some:ns" +--- Re-adding a namespaced attribute --- + + + + +--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNodeNs variation) --- + + + + +--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNode variation) --- + + + + +--- Re-adding a namespaced attribute, with the namespace deleted (appendChild variation) --- + + + + +--- Removing the document reference should not crash --- +string(3) "foo" +string(7) "some:ns" diff --git a/ext/dom/tests/clone_attribute_namespace_02.phpt b/ext/dom/tests/clone_attribute_namespace_02.phpt new file mode 100644 index 0000000000000..382be68cedb95 --- /dev/null +++ b/ext/dom/tests/clone_attribute_namespace_02.phpt @@ -0,0 +1,26 @@ +--TEST-- +Cloning an attribute should retain its namespace 02 +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + + +XML); + +$clone = $dom->documentElement->getAttributeNodeNs("some:ns", "bar")->cloneNode(true); +$dom->documentElement->firstElementChild->setAttributeNodeNs($clone); + +echo $dom->saveXML(); + +?> +--EXPECT-- + + + + diff --git a/ext/dom/tests/import_attribute_namespace.phpt b/ext/dom/tests/import_attribute_namespace.phpt new file mode 100644 index 0000000000000..8fc27eaa5f52c --- /dev/null +++ b/ext/dom/tests/import_attribute_namespace.phpt @@ -0,0 +1,37 @@ +--TEST-- +Cloning an attribute should retain its namespace 02 +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + +XML); + +$dom2 = new DOMDocument; +$dom2->loadXML(<< + +XML); + +$imported = $dom2->importNode($dom->documentElement->getAttributeNodeNs("some:ns", "bar")); +var_dump($imported->prefix, $imported->namespaceURI); +$dom2->documentElement->setAttributeNodeNs($imported); +var_dump($imported->prefix, $imported->namespaceURI); + +echo $dom->saveXML(); +echo $dom2->saveXML(); + +?> +--EXPECT-- +string(7) "default" +string(7) "some:ns" +string(7) "default" +string(7) "some:ns" + + + +