Skip to content

Commit ed6df1f

Browse files
committed
Implement DOMDocument::adoptNode()
For the past 20 years this threw a "not yet implemented" exception. But the function was actually there (albeit not documented) and could be called... Closes phpGH-11333.
1 parent 10f8809 commit ed6df1f

File tree

5 files changed

+216
-14
lines changed

5 files changed

+216
-14
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ PHP NEWS
99
- DOM:
1010
. Fixed bug GH-11500 (Namespace reuse in createElementNS() generates wrong
1111
output). (nielsdos)
12+
. Implemented DOMDocument::adoptNode(). Previously this always threw a
13+
"not yet implemented" exception. (nielsdos)
1214

1315
- Fileinfo:
1416
. Fix GH-11408 (Unable to build PHP 8.3.0 alpha 1 / fileinfo extension).

ext/dom/document.c

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,18 +1051,73 @@ PHP_METHOD(DOMDocument, getElementById)
10511051
}
10521052
/* }}} end dom_document_get_element_by_id */
10531053

1054+
static void php_dom_transfer_document_ref(xmlNodePtr node, dom_object *dom_object_document, xmlDocPtr document)
1055+
{
1056+
if (node->children) {
1057+
php_dom_transfer_document_ref(node->children, dom_object_document, document);
1058+
}
1059+
while (node) {
1060+
php_libxml_node_ptr *iteration_object_ptr = node->_private;
1061+
if (iteration_object_ptr) {
1062+
php_libxml_node_object *iteration_object = iteration_object_ptr->_private;
1063+
ZEND_ASSERT(iteration_object != NULL);
1064+
/* Must increase refcount first because we could be the last reference holder, and the document may be equal. */
1065+
dom_object_document->document->refcount++;
1066+
php_libxml_decrement_doc_ref(iteration_object);
1067+
iteration_object->document = dom_object_document->document;
1068+
}
1069+
node = node->next;
1070+
}
1071+
}
1072+
10541073
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#core-Document3-adoptNode
10551074
Since: DOM Level 3
1075+
Modern spec URL: https://dom.spec.whatwg.org/#dom-document-adoptnode
10561076
*/
10571077
PHP_METHOD(DOMDocument, adoptNode)
10581078
{
1059-
zval *nodep = NULL;
1079+
zval *node_zval;
1080+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &node_zval, dom_node_class_entry) == FAILURE) {
1081+
RETURN_THROWS();
1082+
}
10601083

1061-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &nodep, dom_node_class_entry) == FAILURE) {
1084+
xmlNodePtr nodep;
1085+
dom_object *dom_object_nodep;
1086+
DOM_GET_OBJ(nodep, node_zval, xmlNodePtr, dom_object_nodep);
1087+
1088+
if (UNEXPECTED(nodep->type == XML_DOCUMENT_NODE
1089+
|| nodep->type == XML_HTML_DOCUMENT_NODE
1090+
|| nodep->type == XML_DOCUMENT_TYPE_NODE
1091+
|| nodep->type == XML_DTD_NODE
1092+
|| nodep->type == XML_ENTITY_NODE
1093+
|| nodep->type == XML_NOTATION_NODE)) {
1094+
php_dom_throw_error(NOT_SUPPORTED_ERR, true);
10621095
RETURN_THROWS();
10631096
}
10641097

1065-
DOM_NOT_IMPLEMENTED();
1098+
xmlDocPtr new_document;
1099+
dom_object *dom_object_new_document;
1100+
zval *new_document_zval = ZEND_THIS;
1101+
DOM_GET_OBJ(new_document, new_document_zval, xmlDocPtr, dom_object_new_document);
1102+
1103+
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
1104+
1105+
if (nodep->doc != new_document) {
1106+
php_libxml_invalidate_node_list_cache_from_doc(new_document);
1107+
1108+
/* Note for ATTRIBUTE_NODE: specified is always true in ext/dom,
1109+
* and since this unlink it; the owner element will be unset (i.e. parentNode). */
1110+
int ret = xmlDOMWrapAdoptNode(NULL, nodep->doc, nodep, new_document, NULL, /* options, unused */ 0);
1111+
if (UNEXPECTED(ret != 0)) {
1112+
RETURN_FALSE;
1113+
}
1114+
1115+
php_dom_transfer_document_ref(nodep, dom_object_new_document, new_document);
1116+
} else {
1117+
xmlUnlinkNode(nodep);
1118+
}
1119+
1120+
RETURN_OBJ_COPY(&dom_object_nodep->std);
10661121
}
10671122
/* }}} end dom_document_adopt_node */
10681123

ext/dom/php_dom.stub.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,8 +787,8 @@ public function validate(): bool {}
787787
/** @tentative-return-type */
788788
public function xinclude(int $options = 0): int|false {}
789789

790-
/** @return DOMNode|false */
791-
public function adoptNode(DOMNode $node) {}
790+
/** @tentative-return-type */
791+
public function adoptNode(DOMNode $node): DOMNode|false {}
792792

793793
/** @param DOMNode|string $nodes */
794794
public function append(...$nodes): void {}

ext/dom/php_dom_arginfo.h

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,161 @@
11
--TEST--
2-
DOMDocument::adoptNode not implemented
2+
Tests DOMDocument::adoptNode()
33
--EXTENSIONS--
44
dom
55
--FILE--
66
<?php
77

8-
$dom = new DOMDocument();
9-
$dom->loadXML("<root />");
8+
$doc1 = new DOMDocument();
9+
$doc1->loadXML("<p><b>hi<i attrib=\"1\">x</i></b>world</p>");
10+
$doc2 = new DOMDocument();
11+
$doc2->loadXML("<div/>");
12+
13+
$b_tag_element = $doc1->firstChild->firstChild;
14+
$i_tag_element = $b_tag_element->lastChild;
15+
16+
echo "-- Owner document check before adopting --\n";
17+
var_dump($i_tag_element->ownerDocument === $doc1);
18+
var_dump($i_tag_element->ownerDocument === $doc2);
19+
20+
echo "-- Trying to append child from other document --\n";
21+
try {
22+
$doc2->firstChild->appendChild($b_tag_element); // Should fail because it's another document
23+
} catch (\DOMException $e) {
24+
echo $e->getMessage(), "\n";
25+
}
26+
27+
echo "-- Adopting --\n";
28+
$adopted = $doc2->adoptNode($b_tag_element);
29+
var_dump($adopted->textContent);
30+
var_dump($doc1->saveXML());
31+
var_dump($doc2->saveXML());
32+
33+
echo "-- Appending the adopted node --\n";
34+
35+
$doc2->firstChild->appendChild($adopted);
36+
var_dump($doc2->saveXML());
37+
var_dump($i_tag_element->ownerDocument === $doc1);
38+
var_dump($i_tag_element->ownerDocument === $doc2);
39+
40+
echo "-- Adopt node to the original document --\n";
41+
42+
$adopted = $doc1->adoptNode($doc1->firstChild->firstChild);
43+
var_dump($adopted->textContent);
44+
var_dump($doc1->saveXML());
45+
46+
echo "-- Adopt a document --\n";
1047

1148
try {
12-
$dom->adoptNode($dom->documentElement);
13-
} catch (\Error $e) {
14-
echo $e->getMessage() . \PHP_EOL;
49+
$doc1->adoptNode($doc1);
50+
} catch (\DOMException $e) {
51+
echo $e->getMessage(), "\n";
1552
}
53+
54+
echo "-- Adopt an attribute --\n";
55+
56+
$doc3 = new DOMDocument();
57+
$doc3->loadXML('<p align="center">hi</p>');
58+
$attribute = $doc3->firstChild->attributes->item(0);
59+
var_dump($attribute->parentNode !== NULL);
60+
$adopted = $doc3->adoptNode($attribute);
61+
var_dump($adopted->parentNode === NULL);
62+
echo $doc3->saveXML();
63+
64+
echo "-- Append an attribute from another document --\n";
65+
66+
$doc4 = new DOMDocument();
67+
$doc4->appendChild($doc4->createElement('container'));
68+
$doc4->documentElement->appendChild($doc4->adoptNode($adopted));
69+
echo $doc4->saveXML();
70+
71+
echo "-- Adopt an entity reference --\n";
72+
73+
$doc4 = new DOMDocument();
74+
$doc4->loadXML(<<<'XML'
75+
<?xml version='1.0' encoding='utf-8' ?>
76+
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
77+
<!ENTITY my_entity '<p>hi</p>'> ]>
78+
<p/>
79+
XML, LIBXML_NOENT);
80+
$p_tag_element = $doc4->firstChild->nextSibling;
81+
$entity_reference = $doc4->createEntityReference('my_entity');
82+
$p_tag_element->appendChild($entity_reference);
83+
var_dump($doc4->saveXML());
84+
$doc3->adoptNode($entity_reference);
85+
var_dump($doc4->saveXML());
86+
$doc3->firstChild->appendChild($entity_reference);
87+
var_dump($doc3->saveXML());
88+
89+
echo "-- Adopt a node and destroy the new document --\n";
90+
91+
$doc1 = new DOMDocument();
92+
$doc1->appendChild($doc1->createElement('child'));
93+
$doc2 = new DOMDocument();
94+
$doc2->appendChild($doc2->createElement('container'));
95+
$doc2->documentElement->appendChild($child = $doc2->adoptNode($doc1->documentElement));
96+
echo $doc1->saveXML();
97+
echo $doc2->saveXML();
98+
// Try to trigger a use-after-free
99+
unset($doc2);
100+
var_dump($child->nodeName);
101+
unset($doc1);
102+
var_dump($child->nodeName);
103+
16104
?>
17105
--EXPECT--
18-
Not yet implemented
106+
-- Owner document check before adopting --
107+
bool(true)
108+
bool(false)
109+
-- Trying to append child from other document --
110+
Wrong Document Error
111+
-- Adopting --
112+
string(3) "hix"
113+
string(35) "<?xml version="1.0"?>
114+
<p>world</p>
115+
"
116+
string(29) "<?xml version="1.0"?>
117+
<div/>
118+
"
119+
-- Appending the adopted node --
120+
string(62) "<?xml version="1.0"?>
121+
<div><b>hi<i attrib="1">x</i></b></div>
122+
"
123+
bool(false)
124+
bool(true)
125+
-- Adopt node to the original document --
126+
string(5) "world"
127+
string(27) "<?xml version="1.0"?>
128+
<p/>
129+
"
130+
-- Adopt a document --
131+
Not Supported Error
132+
-- Adopt an attribute --
133+
bool(true)
134+
bool(true)
135+
<?xml version="1.0"?>
136+
<p>hi</p>
137+
-- Append an attribute from another document --
138+
<?xml version="1.0"?>
139+
<container align="center"/>
140+
-- Adopt an entity reference --
141+
string(202) "<?xml version="1.0" encoding="utf-8"?>
142+
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
143+
<!ENTITY my_entity "<p>hi</p>">
144+
]>
145+
<p>&my_entity;</p>
146+
"
147+
string(188) "<?xml version="1.0" encoding="utf-8"?>
148+
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
149+
<!ENTITY my_entity "<p>hi</p>">
150+
]>
151+
<p/>
152+
"
153+
string(43) "<?xml version="1.0"?>
154+
<p>hi&my_entity;</p>
155+
"
156+
-- Adopt a node and destroy the new document --
157+
<?xml version="1.0"?>
158+
<?xml version="1.0"?>
159+
<container><child/></container>
160+
string(5) "child"
161+
string(5) "child"

0 commit comments

Comments
 (0)