diff --git a/.github/actions/test-linux/action.yml b/.github/actions/test-linux/action.yml index 9b7d0d100608f..36542cffb89a5 100644 --- a/.github/actions/test-linux/action.yml +++ b/.github/actions/test-linux/action.yml @@ -40,7 +40,7 @@ runs: --offline \ --show-diff \ --show-slow 1000 \ - --set-timeout 120 + --set-timeout 120 ext/dom - uses: actions/upload-artifact@v3 if: always() && inputs.testArtifacts != null with: diff --git a/NEWS b/NEWS index ae1d2c2cacaf4..e329624625dd3 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-11507 (String concatenation performance regression in 8.3). (nielsdos) +- DOM: + . 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). (nielsdos) diff --git a/UPGRADING b/UPGRADING index 12189958faaeb..bc7e50760be7b 100644 --- a/UPGRADING +++ b/UPGRADING @@ -130,6 +130,8 @@ PHP 8.3 UPGRADE NOTES - Dom: . Changed DOMCharacterData::appendData() tentative return type to true. + . Implemented DOMDocument::adoptNode(). Previously this always threw a + "not yet implemented" exception. - Gd: . Changed imagerotate signature, removed the `ignore_transparent` argument diff --git a/ext/dom/document.c b/ext/dom/document.c index 1b26c9c7bfc73..fa32452c8540d 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -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; - - if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &nodep, dom_node_class_entry) == FAILURE) { + zval *node_zval; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &node_zval, dom_node_class_entry) == FAILURE) { RETURN_THROWS(); } - DOM_NOT_IMPLEMENTED(); + 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; + } + + 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 */ diff --git a/ext/dom/tests/DOMDocument_adoptNode.phpt b/ext/dom/tests/DOMDocument_adoptNode.phpt index fbc6ff2205e8c..e2f5c0bf431ee 100644 --- a/ext/dom/tests/DOMDocument_adoptNode.phpt +++ b/ext/dom/tests/DOMDocument_adoptNode.phpt @@ -1,18 +1,141 @@ --TEST-- -DOMDocument::adoptNode not implemented +Tests DOMDocument::adoptNode() --EXTENSIONS-- dom --FILE-- loadXML(""); +$doc1 = new DOMDocument(); +$doc1->loadXML("

hixworld

"); +$doc2 = new DOMDocument(); +$doc2->loadXML("
"); +$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 { - $dom->adoptNode($dom->documentElement); -} catch (\Error $e) { - echo $e->getMessage() . \PHP_EOL; + $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 { + $doc1->adoptNode($doc1); +} catch (\DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo "-- Adopt an attribute --\n"; + +$doc3 = new DOMDocument(); +$doc3->loadXML('

hi

'); +$attribute = $doc3->firstChild->attributes->item(0); +var_dump($attribute->parentNode !== NULL); +$adopted = $doc3->adoptNode($attribute); +var_dump($adopted->parentNode === NULL); + +echo "-- Adopt an entity reference --\n"; + +$doc4 = new DOMDocument(); +$doc4->loadXML(<<<'XML' + +hi

'> ]> +

+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)); +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) " +

world

+" +string(29) " +
+" +-- Appending the adopted node -- +string(62) " +
hix
+" +bool(false) +bool(true) +-- Adopt node to the original document -- +string(5) "world" +string(27) " +

+" +-- Adopt a document -- +Not Supported Error +-- Adopt an attribute -- +bool(true) +bool(true) +-- Adopt an entity reference -- +string(202) " +hi

"> +]> +

&my_entity;

+" +string(188) " +hi

"> +]> +

+" +string(43) " +

hi&my_entity;

+" +-- Adopt a node and destroy the new document -- +string(5) "child" +string(5) "child"