Skip to content

Commit

Permalink
Implement DOMDocument::adoptNode()
Browse files Browse the repository at this point in the history
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 GH-11333.
  • Loading branch information
nielsdos committed Jun 27, 2023
1 parent 10f8809 commit ed6df1f
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 14 deletions.
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ PHP NEWS
- DOM:
. Fixed bug GH-11500 (Namespace reuse in createElementNS() generates wrong
output). (nielsdos)
. Implemented DOMDocument::adoptNode(). Previously this always threw a
"not yet implemented" exception. (nielsdos)

- Fileinfo:
. Fix GH-11408 (Unable to build PHP 8.3.0 alpha 1 / fileinfo extension).
Expand Down
61 changes: 58 additions & 3 deletions ext/dom/document.c
Original file line number Diff line number Diff line change
Expand Up @@ -1051,18 +1051,73 @@ PHP_METHOD(DOMDocument, getElementById)
}
/* }}} end dom_document_get_element_by_id */

static void php_dom_transfer_document_ref(xmlNodePtr node, dom_object *dom_object_document, xmlDocPtr document)
{
if (node->children) {
php_dom_transfer_document_ref(node->children, dom_object_document, document);
}
while (node) {
php_libxml_node_ptr *iteration_object_ptr = node->_private;
if (iteration_object_ptr) {
php_libxml_node_object *iteration_object = iteration_object_ptr->_private;
ZEND_ASSERT(iteration_object != NULL);
/* Must increase refcount first because we could be the last reference holder, and the document may be equal. */
dom_object_document->document->refcount++;
php_libxml_decrement_doc_ref(iteration_object);
iteration_object->document = dom_object_document->document;
}
node = node->next;
}
}

/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#core-Document3-adoptNode
Since: DOM Level 3
Modern spec URL: https://dom.spec.whatwg.org/#dom-document-adoptnode
*/
PHP_METHOD(DOMDocument, adoptNode)
{
zval *nodep = NULL;
zval *node_zval;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &node_zval, dom_node_class_entry) == FAILURE) {
RETURN_THROWS();
}

if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &nodep, dom_node_class_entry) == FAILURE) {
xmlNodePtr nodep;
dom_object *dom_object_nodep;
DOM_GET_OBJ(nodep, node_zval, xmlNodePtr, dom_object_nodep);

if (UNEXPECTED(nodep->type == XML_DOCUMENT_NODE
|| nodep->type == XML_HTML_DOCUMENT_NODE
|| nodep->type == XML_DOCUMENT_TYPE_NODE
|| nodep->type == XML_DTD_NODE
|| nodep->type == XML_ENTITY_NODE
|| nodep->type == XML_NOTATION_NODE)) {
php_dom_throw_error(NOT_SUPPORTED_ERR, true);
RETURN_THROWS();
}

DOM_NOT_IMPLEMENTED();
xmlDocPtr new_document;
dom_object *dom_object_new_document;
zval *new_document_zval = ZEND_THIS;
DOM_GET_OBJ(new_document, new_document_zval, xmlDocPtr, dom_object_new_document);

php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);

if (nodep->doc != new_document) {
php_libxml_invalidate_node_list_cache_from_doc(new_document);

/* Note for ATTRIBUTE_NODE: specified is always true in ext/dom,
* and since this unlink it; the owner element will be unset (i.e. parentNode). */
int ret = xmlDOMWrapAdoptNode(NULL, nodep->doc, nodep, new_document, NULL, /* options, unused */ 0);
if (UNEXPECTED(ret != 0)) {
RETURN_FALSE;
}

php_dom_transfer_document_ref(nodep, dom_object_new_document, new_document);
} else {
xmlUnlinkNode(nodep);
}

RETURN_OBJ_COPY(&dom_object_nodep->std);
}
/* }}} end dom_document_adopt_node */

Expand Down
4 changes: 2 additions & 2 deletions ext/dom/php_dom.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,8 @@ public function validate(): bool {}
/** @tentative-return-type */
public function xinclude(int $options = 0): int|false {}

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

/** @param DOMNode|string $nodes */
public function append(...$nodes): void {}
Expand Down
6 changes: 4 additions & 2 deletions ext/dom/php_dom_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 150 additions & 7 deletions ext/dom/tests/DOMDocument_adoptNode.phpt
Original file line number Diff line number Diff line change
@@ -1,18 +1,161 @@
--TEST--
DOMDocument::adoptNode not implemented
Tests DOMDocument::adoptNode()
--EXTENSIONS--
dom
--FILE--
<?php

$dom = new DOMDocument();
$dom->loadXML("<root />");
$doc1 = new DOMDocument();
$doc1->loadXML("<p><b>hi<i attrib=\"1\">x</i></b>world</p>");
$doc2 = new DOMDocument();
$doc2->loadXML("<div/>");

$b_tag_element = $doc1->firstChild->firstChild;
$i_tag_element = $b_tag_element->lastChild;

echo "-- Owner document check before adopting --\n";
var_dump($i_tag_element->ownerDocument === $doc1);
var_dump($i_tag_element->ownerDocument === $doc2);

echo "-- Trying to append child from other document --\n";
try {
$doc2->firstChild->appendChild($b_tag_element); // Should fail because it's another document
} catch (\DOMException $e) {
echo $e->getMessage(), "\n";
}

echo "-- Adopting --\n";
$adopted = $doc2->adoptNode($b_tag_element);
var_dump($adopted->textContent);
var_dump($doc1->saveXML());
var_dump($doc2->saveXML());

echo "-- Appending the adopted node --\n";

$doc2->firstChild->appendChild($adopted);
var_dump($doc2->saveXML());
var_dump($i_tag_element->ownerDocument === $doc1);
var_dump($i_tag_element->ownerDocument === $doc2);

echo "-- Adopt node to the original document --\n";

$adopted = $doc1->adoptNode($doc1->firstChild->firstChild);
var_dump($adopted->textContent);
var_dump($doc1->saveXML());

echo "-- Adopt a document --\n";

try {
$dom->adoptNode($dom->documentElement);
} catch (\Error $e) {
echo $e->getMessage() . \PHP_EOL;
$doc1->adoptNode($doc1);
} catch (\DOMException $e) {
echo $e->getMessage(), "\n";
}

echo "-- Adopt an attribute --\n";

$doc3 = new DOMDocument();
$doc3->loadXML('<p align="center">hi</p>');
$attribute = $doc3->firstChild->attributes->item(0);
var_dump($attribute->parentNode !== NULL);
$adopted = $doc3->adoptNode($attribute);
var_dump($adopted->parentNode === NULL);
echo $doc3->saveXML();

echo "-- Append an attribute from another document --\n";

$doc4 = new DOMDocument();
$doc4->appendChild($doc4->createElement('container'));
$doc4->documentElement->appendChild($doc4->adoptNode($adopted));
echo $doc4->saveXML();

echo "-- Adopt an entity reference --\n";

$doc4 = new DOMDocument();
$doc4->loadXML(<<<'XML'
<?xml version='1.0' encoding='utf-8' ?>
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
<!ENTITY my_entity '<p>hi</p>'> ]>
<p/>
XML, LIBXML_NOENT);
$p_tag_element = $doc4->firstChild->nextSibling;
$entity_reference = $doc4->createEntityReference('my_entity');
$p_tag_element->appendChild($entity_reference);
var_dump($doc4->saveXML());
$doc3->adoptNode($entity_reference);
var_dump($doc4->saveXML());
$doc3->firstChild->appendChild($entity_reference);
var_dump($doc3->saveXML());

echo "-- Adopt a node and destroy the new document --\n";

$doc1 = new DOMDocument();
$doc1->appendChild($doc1->createElement('child'));
$doc2 = new DOMDocument();
$doc2->appendChild($doc2->createElement('container'));
$doc2->documentElement->appendChild($child = $doc2->adoptNode($doc1->documentElement));
echo $doc1->saveXML();
echo $doc2->saveXML();
// Try to trigger a use-after-free
unset($doc2);
var_dump($child->nodeName);
unset($doc1);
var_dump($child->nodeName);

?>
--EXPECT--
Not yet implemented
-- Owner document check before adopting --
bool(true)
bool(false)
-- Trying to append child from other document --
Wrong Document Error
-- Adopting --
string(3) "hix"
string(35) "<?xml version="1.0"?>
<p>world</p>
"
string(29) "<?xml version="1.0"?>
<div/>
"
-- Appending the adopted node --
string(62) "<?xml version="1.0"?>
<div><b>hi<i attrib="1">x</i></b></div>
"
bool(false)
bool(true)
-- Adopt node to the original document --
string(5) "world"
string(27) "<?xml version="1.0"?>
<p/>
"
-- Adopt a document --
Not Supported Error
-- Adopt an attribute --
bool(true)
bool(true)
<?xml version="1.0"?>
<p>hi</p>
-- Append an attribute from another document --
<?xml version="1.0"?>
<container align="center"/>
-- Adopt an entity reference --
string(202) "<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
<!ENTITY my_entity "<p>hi</p>">
]>
<p>&my_entity;</p>
"
string(188) "<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE set PUBLIC "-//OASIS//DTD DocBook XML V5.0//EN" "http://www.docbook.org/xml/5.0/dtd/docbook.dtd" [
<!ENTITY my_entity "<p>hi</p>">
]>
<p/>
"
string(43) "<?xml version="1.0"?>
<p>hi&my_entity;</p>
"
-- Adopt a node and destroy the new document --
<?xml version="1.0"?>
<?xml version="1.0"?>
<container><child/></container>
string(5) "child"
string(5) "child"

0 comments on commit ed6df1f

Please sign in to comment.