Skip to content

Fix cloning attribute with namespace disappearing namespace #12547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ext/dom/element.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions ext/dom/node.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions ext/dom/php_dom.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions ext/dom/php_dom.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ $dom1->loadXML('<?xml version="1.0"?><container xmlns:foo="http://php.net" foo:b
$dom2->loadXML('<?xml version="1.0"?><container xmlns="http://php.net" xmlns:foo="http://php.net/2"/>');
$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();
Expand Down Expand Up @@ -54,6 +56,10 @@ echo $dom2->saveXML();
<?xml version="1.0"?>
<container xmlns:foo="http://php.net/2" xmlns:default="http://php.net" default:bar="yes"/>
--- 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"
<?xml version="1.0"?>
<container xmlns:foo="http://php.net" foo:bar="yes"/>
<?xml version="1.0"?>
Expand Down
84 changes: 84 additions & 0 deletions ext/dom/tests/clone_attribute_namespace_01.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
--TEST--
Cloning an attribute should retain its namespace 01
--EXTENSIONS--
dom
--FILE--
<?php

function createTestDocument() {
$dom = new DOMDocument;
$dom->loadXML('<?xml version="1.0"?><container/>');
$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 ---
<?xml version="1.0"?>
<container xmlns:foo="some:ns"/>
<?xml version="1.0"?>
<container xmlns:foo="some:ns" foo:bar="value"/>
--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNodeNs variation) ---
<?xml version="1.0"?>
<container/>
<?xml version="1.0"?>
<container><child xmlns:foo="some:ns" foo:bar="value"/></container>
--- Re-adding a namespaced attribute, with the namespace deleted (setAttributeNode variation) ---
<?xml version="1.0"?>
<container/>
<?xml version="1.0"?>
<container><child xmlns:foo="some:ns" foo:bar="value"/></container>
--- Re-adding a namespaced attribute, with the namespace deleted (appendChild variation) ---
<?xml version="1.0"?>
<container/>
<?xml version="1.0"?>
<container><child xmlns:foo="some:ns" foo:bar="value"/></container>
--- Removing the document reference should not crash ---
string(3) "foo"
string(7) "some:ns"
26 changes: 26 additions & 0 deletions ext/dom/tests/clone_attribute_namespace_02.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Cloning an attribute should retain its namespace 02
--EXTENSIONS--
dom
--FILE--
<?php

$dom = new DOMDocument;
$dom->loadXML(<<<XML
<?xml version="1.0"?>
<container xmlns:foo="some:ns" foo:bar="1">
<child xmlns:foo="some:other"/>
</container>
XML);

$clone = $dom->documentElement->getAttributeNodeNs("some:ns", "bar")->cloneNode(true);
$dom->documentElement->firstElementChild->setAttributeNodeNs($clone);

echo $dom->saveXML();

?>
--EXPECT--
<?xml version="1.0"?>
<container xmlns:foo="some:ns" foo:bar="1">
<child xmlns:foo="some:other" xmlns:foo1="some:ns" foo1:bar="1"/>
</container>
37 changes: 37 additions & 0 deletions ext/dom/tests/import_attribute_namespace.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
Cloning an attribute should retain its namespace 02
--EXTENSIONS--
dom
--FILE--
<?php

$dom = new DOMDocument;
$dom->loadXML(<<<XML
<?xml version="1.0"?>
<container xmlns:foo="some:ns" foo:bar="1"/>
XML);

$dom2 = new DOMDocument;
$dom2->loadXML(<<<XML
<?xml version="1.0"?>
<container xmlns:foo="some:other"/>
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"
<?xml version="1.0"?>
<container xmlns:foo="some:ns" foo:bar="1"/>
<?xml version="1.0"?>
<container xmlns:foo="some:other" xmlns:default="some:ns" default:bar="1"/>