From 6560c9bf8e816e9234902f6d9b02309e3389c4ea Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:55:25 +0200 Subject: [PATCH] Implement DOMParentNode::replaceChildren() ref: https://dom.spec.whatwg.org/#dom-parentnode-replacechildren --- NEWS | 1 + UPGRADING | 1 + ext/dom/document.c | 19 +++++ ext/dom/documentfragment.c | 19 +++++ ext/dom/element.c | 19 +++++ ext/dom/parentnode.c | 33 ++++++++ ext/dom/php_dom.h | 1 + ext/dom/php_dom.stub.php | 12 +++ ext/dom/php_dom_arginfo.h | 17 +++- ext/dom/tests/DOMElement_replaceChildren.phpt | 81 +++++++++++++++++++ 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/DOMElement_replaceChildren.phpt diff --git a/NEWS b/NEWS index 91cdacbe186b3..1daeb5be958c4 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,7 @@ PHP NEWS . Added DOMElement::getAttributeNames(). (nielsdos) . Added DOMNode::getRootNode(). (nielsdos) . Added DOMElement::className. (nielsdos) + . Added DOMParentNode::replaceChildren(). (nielsdos) - Intl: . Fix memory leak in MessageFormatter::format() on failure. (Girgias) diff --git a/UPGRADING b/UPGRADING index cbe3ce143c128..fbac6071ed352 100644 --- a/UPGRADING +++ b/UPGRADING @@ -254,6 +254,7 @@ PHP 8.3 UPGRADE NOTES yet. . Added DOMElement::className. This is not binary-safe at the moment because of underlying limitations of libxml2. + . Added DOMParentNode::replaceChildren(). - JSON: . Added json_validate(), which returns whether the json is valid for diff --git a/ext/dom/document.c b/ext/dom/document.c index 45db1d81feecf..e26b3e2b0dcb8 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -2178,4 +2178,23 @@ PHP_METHOD(DOMDocument, prepend) } /* }}} */ +/* {{{ URL: https://dom.spec.whatwg.org/#dom-parentnode-replacechildren +Since: +*/ +PHP_METHOD(DOMDocument, replaceChildren) +{ + uint32_t argc; + zval *args; + dom_object *intern; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "*", &args, &argc) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_INTERN(intern); + + dom_parent_node_replace_children(intern, args, argc); +} +/* }}} */ + #endif /* HAVE_LIBXML && HAVE_DOM */ diff --git a/ext/dom/documentfragment.c b/ext/dom/documentfragment.c index e3be70228744b..379cf6699b601 100644 --- a/ext/dom/documentfragment.c +++ b/ext/dom/documentfragment.c @@ -130,4 +130,23 @@ PHP_METHOD(DOMDocumentFragment, prepend) } /* }}} */ +/* {{{ URL: https://dom.spec.whatwg.org/#dom-parentnode-replacechildren +Since: +*/ +PHP_METHOD(DOMDocumentFragment, replaceChildren) +{ + uint32_t argc; + zval *args; + dom_object *intern; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "*", &args, &argc) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_INTERN(intern); + + dom_parent_node_replace_children(intern, args, argc); +} +/* }}} */ + #endif diff --git a/ext/dom/element.c b/ext/dom/element.c index 2ca3ef2f06692..c52471c89fa3a 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -1295,4 +1295,23 @@ PHP_METHOD(DOMElement, replaceWith) } /* }}} end DOMElement::prepend */ +/* {{{ URL: https://dom.spec.whatwg.org/#dom-parentnode-replacechildren +Since: +*/ +PHP_METHOD(DOMElement, replaceChildren) +{ + uint32_t argc; + zval *args; + dom_object *intern; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "*", &args, &argc) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_INTERN(intern); + + dom_parent_node_replace_children(intern, args, argc); +} +/* }}} */ + #endif diff --git a/ext/dom/parentnode.c b/ext/dom/parentnode.c index 8ef5645b0ca3c..ffccc78652cd2 100644 --- a/ext/dom/parentnode.c +++ b/ext/dom/parentnode.c @@ -591,4 +591,37 @@ void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc) xmlFree(fragment); } +void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t nodesc) +{ + /* Spec link: https://dom.spec.whatwg.org/#dom-parentnode-replacechildren */ + + xmlNodePtr thisp = dom_object_get_node(context); + /* Note: Only rule 2 of pre-insertion validity can be broken */ + if (dom_hierarchy_node_list(thisp, nodes, nodesc)) { + php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(context->document)); + return; + } + + xmlNodePtr fragment = dom_zvals_to_fragment(context->document, thisp, nodes, nodesc); + if (UNEXPECTED(fragment == NULL)) { + return; + } + + php_libxml_invalidate_node_list_cache_from_doc(context->document->ptr); + + dom_remove_all_children(thisp); + + xmlNodePtr newchild = fragment->children; + if (newchild) { + xmlNodePtr last = fragment->last; + + dom_pre_insert(NULL, thisp, newchild, fragment); + + dom_fragment_assign_parent_node(thisp, fragment); + dom_reconcile_ns_list(thisp->doc, newchild, last); + } + + xmlFree(fragment); +} + #endif diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 0016e44ca1789..b2c102fde45da 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -155,6 +155,7 @@ void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc); void dom_parent_node_append(dom_object *context, zval *nodes, uint32_t nodesc); void dom_parent_node_after(dom_object *context, zval *nodes, uint32_t nodesc); void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc); +void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t nodesc); void dom_child_node_remove(dom_object *context); void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc); diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index 97ce8f487354c..a6ac588e3c046 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -275,6 +275,9 @@ public function append(...$nodes): void; /** @param DOMNode|string $nodes */ public function prepend(...$nodes): void; + + /** @param DOMNode|string $nodes */ + public function replaceChildren(...$nodes): void; } interface DOMChildNode @@ -459,6 +462,9 @@ public function append(...$nodes): void {} /** @param DOMNode|string $nodes */ public function prepend(...$nodes): void {} + + /** @param DOMNode|string $nodes */ + public function replaceChildren(...$nodes): void {} } class DOMNodeList implements IteratorAggregate, Countable @@ -636,6 +642,9 @@ public function append(...$nodes): void {} /** @param DOMNode|string $nodes */ public function prepend(...$nodes): void {} + + /** @param DOMNode|string $nodes */ + public function replaceChildren(...$nodes): void {} } class DOMDocument extends DOMNode implements DOMParentNode @@ -803,6 +812,9 @@ public function append(...$nodes): void {} /** @param DOMNode|string $nodes */ public function prepend(...$nodes): void {} + + /** @param DOMNode|string $nodes */ + public function replaceChildren(...$nodes): void {} } final class DOMException extends Exception diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index 9f39a731e292a..223bd06ff5a16 100644 --- a/ext/dom/php_dom_arginfo.h +++ b/ext/dom/php_dom_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 91732c51635f43f016f4b531d9aa8e00312084ec */ + * Stub hash: 5ce5f3c6715ab8ee1d8af7b03f2d2fecc757e7e0 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -19,6 +19,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_DOMParentNode_prepend arginfo_class_DOMParentNode_append +#define arginfo_class_DOMParentNode_replaceChildren arginfo_class_DOMParentNode_append + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMChildNode_remove, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() @@ -138,6 +140,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_DOMDocumentFragment_prepend arginfo_class_DOMParentNode_append +#define arginfo_class_DOMDocumentFragment_replaceChildren arginfo_class_DOMParentNode_append + #define arginfo_class_DOMNodeList_count arginfo_class_DOMNode_getLineNo ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_DOMNodeList_getIterator, 0, 0, Iterator, 0) @@ -286,6 +290,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_DOMElement_prepend arginfo_class_DOMParentNode_append +#define arginfo_class_DOMElement_replaceChildren arginfo_class_DOMParentNode_append + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_DOMDocument___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, version, IS_STRING, 0, "\"1.0\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 0, "\"\"") @@ -439,6 +445,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_DOMDocument_prepend arginfo_class_DOMParentNode_append +#define arginfo_class_DOMDocument_replaceChildren arginfo_class_DOMParentNode_append + #define arginfo_class_DOMText___construct arginfo_class_DOMComment___construct #define arginfo_class_DOMText_isWhitespaceInElementContent arginfo_class_DOMNode_hasAttributes @@ -533,6 +541,7 @@ ZEND_METHOD(DOMDocumentFragment, __construct); ZEND_METHOD(DOMDocumentFragment, appendXML); ZEND_METHOD(DOMDocumentFragment, append); ZEND_METHOD(DOMDocumentFragment, prepend); +ZEND_METHOD(DOMDocumentFragment, replaceChildren); ZEND_METHOD(DOMNodeList, count); ZEND_METHOD(DOMNodeList, getIterator); ZEND_METHOD(DOMNodeList, item); @@ -573,6 +582,7 @@ ZEND_METHOD(DOMElement, after); ZEND_METHOD(DOMElement, replaceWith); ZEND_METHOD(DOMElement, append); ZEND_METHOD(DOMElement, prepend); +ZEND_METHOD(DOMElement, replaceChildren); ZEND_METHOD(DOMDocument, __construct); ZEND_METHOD(DOMDocument, createAttribute); ZEND_METHOD(DOMDocument, createAttributeNS); @@ -623,6 +633,7 @@ ZEND_METHOD(DOMDocument, xinclude); ZEND_METHOD(DOMDocument, adoptNode); ZEND_METHOD(DOMDocument, append); ZEND_METHOD(DOMDocument, prepend); +ZEND_METHOD(DOMDocument, replaceChildren); ZEND_METHOD(DOMText, __construct); ZEND_METHOD(DOMText, isWhitespaceInElementContent); ZEND_METHOD(DOMText, splitText); @@ -676,6 +687,7 @@ static const zend_function_entry class_DOMComment_methods[] = { static const zend_function_entry class_DOMParentNode_methods[] = { ZEND_ABSTRACT_ME_WITH_FLAGS(DOMParentNode, append, arginfo_class_DOMParentNode_append, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) ZEND_ABSTRACT_ME_WITH_FLAGS(DOMParentNode, prepend, arginfo_class_DOMParentNode_prepend, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) + ZEND_ABSTRACT_ME_WITH_FLAGS(DOMParentNode, replaceChildren, arginfo_class_DOMParentNode_replaceChildren, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT) ZEND_FE_END }; @@ -732,6 +744,7 @@ static const zend_function_entry class_DOMDocumentFragment_methods[] = { ZEND_ME(DOMDocumentFragment, appendXML, arginfo_class_DOMDocumentFragment_appendXML, ZEND_ACC_PUBLIC) ZEND_ME(DOMDocumentFragment, append, arginfo_class_DOMDocumentFragment_append, ZEND_ACC_PUBLIC) ZEND_ME(DOMDocumentFragment, prepend, arginfo_class_DOMDocumentFragment_prepend, ZEND_ACC_PUBLIC) + ZEND_ME(DOMDocumentFragment, replaceChildren, arginfo_class_DOMDocumentFragment_replaceChildren, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -792,6 +805,7 @@ static const zend_function_entry class_DOMElement_methods[] = { ZEND_ME(DOMElement, replaceWith, arginfo_class_DOMElement_replaceWith, ZEND_ACC_PUBLIC) ZEND_ME(DOMElement, append, arginfo_class_DOMElement_append, ZEND_ACC_PUBLIC) ZEND_ME(DOMElement, prepend, arginfo_class_DOMElement_prepend, ZEND_ACC_PUBLIC) + ZEND_ME(DOMElement, replaceChildren, arginfo_class_DOMElement_replaceChildren, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -847,6 +861,7 @@ static const zend_function_entry class_DOMDocument_methods[] = { ZEND_ME(DOMDocument, adoptNode, arginfo_class_DOMDocument_adoptNode, ZEND_ACC_PUBLIC) ZEND_ME(DOMDocument, append, arginfo_class_DOMDocument_append, ZEND_ACC_PUBLIC) ZEND_ME(DOMDocument, prepend, arginfo_class_DOMDocument_prepend, ZEND_ACC_PUBLIC) + ZEND_ME(DOMDocument, replaceChildren, arginfo_class_DOMDocument_replaceChildren, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/dom/tests/DOMElement_replaceChildren.phpt b/ext/dom/tests/DOMElement_replaceChildren.phpt new file mode 100644 index 0000000000000..a28d99934183c --- /dev/null +++ b/ext/dom/tests/DOMElement_replaceChildren.phpt @@ -0,0 +1,81 @@ +--TEST-- +DOMParentNode::replaceChildren() +--EXTENSIONS-- +dom +--FILE-- +loadHTML('

hi

test

hi2

'); + +echo "--- Edge cases ---\n"; + +try { + $dom->documentElement->replaceChildren($dom->documentElement); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +try { + $dom->documentElement->firstElementChild->replaceChildren($dom->documentElement); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo "--- Normal cases ---\n"; + +$dom->documentElement->replaceChildren('foo', $dom->createElement('p'), 'bar'); +echo $dom->saveXML(); + +$fragment1 = $dom->createDocumentFragment(); +$fragment1->appendChild($dom->createElement('a')); +$fragment1->appendChild($dom->createElement('b')); +$fragment2 = $dom->createDocumentFragment(); +$fragment2->append('text'); +$fragment3 = $dom->createDocumentFragment(); +$dom->documentElement->replaceChildren($fragment1, $fragment2, $fragment3); +echo $dom->saveXML(); + +echo "--- Fragment case ---\n"; + +$fragment = $dom->createDocumentFragment(); +$fragment->replaceChildren(); +var_dump($dom->saveXML($fragment)); + +$old = $fragment->appendChild($dom->createElement('p', 'test')); +$fragment->replaceChildren($dom->createElement('b', 'test')); +echo $dom->saveXML($fragment), "\n"; +var_dump($old->nodeValue); + +echo "--- Idempotent case ---\n"; + +$dom->replaceChildren($dom->documentElement); +echo $dom->saveXML(); + +echo "--- Removal shortcut ---\n"; + +$dom->documentElement->replaceChildren(); +echo $dom->saveXML(); + +?> +--EXPECT-- +--- Edge cases --- +Hierarchy Request Error +Hierarchy Request Error +--- Normal cases --- + + +foo

bar + + +text +--- Fragment case --- +string(0) "" +test +string(4) "test" +--- Idempotent case --- + +text +--- Removal shortcut --- + +