diff --git a/NEWS b/NEWS index 6ca2a85f72c03..32cd1b6b1dc54 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,8 @@ PHP NEWS . Added DOMNode::parentElement and DOMNameSpaceNode::parentElement. (nielsdos) . Added DOMNode::isEqualNode(). (nielsdos) + . Added DOMElement::insertAdjacentElement() and + DOMElement::insertAdjacentText(). (nielsdos) - FPM: . Added warning to log when fpm socket was not registered on the expected diff --git a/UPGRADING b/UPGRADING index 18700b6872c2c..0750bb08532c3 100644 --- a/UPGRADING +++ b/UPGRADING @@ -271,6 +271,8 @@ PHP 8.3 UPGRADE NOTES . Added DOMNode::isConnected and DOMNameSpaceNode::isConnected. . Added DOMNode::parentElement and DOMNameSpaceNode::parentElement. . Added DOMNode::isEqualNode(). + . Added DOMElement::insertAdjacentElement() and + DOMElement::insertAdjacentText(). - 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 1d7f62678b562..b7bd36b8b05d2 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -1050,6 +1050,26 @@ static void php_dom_transfer_document_ref(xmlNodePtr node, dom_object *dom_objec } } +bool php_dom_adopt_node(xmlNodePtr nodep, dom_object *dom_object_new_document, xmlDocPtr 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 true; +} + /* {{{ 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 @@ -1080,21 +1100,8 @@ PHP_METHOD(DOMDocument, adoptNode) 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); + if (!php_dom_adopt_node(nodep, dom_object_new_document, new_document)) { + RETURN_FALSE; } RETURN_OBJ_COPY(&dom_object_nodep->std); diff --git a/ext/dom/element.c b/ext/dom/element.c index e0cefe0a79f88..8acbac3f964e0 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -1345,4 +1345,120 @@ PHP_METHOD(DOMElement, replaceChildren) } /* }}} */ +#define INSERT_ADJACENT_RES_FAILED ((void*) -1) + +static xmlNodePtr dom_insert_adjacent(const zend_string *where, xmlNodePtr thisp, dom_object *this_intern, xmlNodePtr otherp) +{ + if (zend_string_equals_literal_ci(where, "beforebegin")) { + if (thisp->parent == NULL) { + return NULL; + } + if (dom_hierarchy(thisp->parent, otherp) == FAILURE) { + php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(this_intern->document)); + return INSERT_ADJACENT_RES_FAILED; + } + if (!php_dom_adopt_node(otherp, this_intern, thisp->doc)) { + return INSERT_ADJACENT_RES_FAILED; + } + otherp = xmlAddPrevSibling(thisp, otherp); + } else if (zend_string_equals_literal_ci(where, "afterbegin")) { + if (dom_hierarchy(thisp, otherp) == FAILURE) { + php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(this_intern->document)); + return INSERT_ADJACENT_RES_FAILED; + } + if (!php_dom_adopt_node(otherp, this_intern, thisp->doc)) { + return INSERT_ADJACENT_RES_FAILED; + } + if (thisp->children == NULL) { + otherp = xmlAddChild(thisp, otherp); + } else { + otherp = xmlAddPrevSibling(thisp->children, otherp); + } + } else if (zend_string_equals_literal_ci(where, "beforeend")) { + if (dom_hierarchy(thisp, otherp) == FAILURE) { + php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(this_intern->document)); + return INSERT_ADJACENT_RES_FAILED; + } + if (!php_dom_adopt_node(otherp, this_intern, thisp->doc)) { + return INSERT_ADJACENT_RES_FAILED; + } + otherp = xmlAddChild(thisp, otherp); + } else if (zend_string_equals_literal_ci(where, "afterend")) { + if (thisp->parent == NULL) { + return NULL; + } + if (dom_hierarchy(thisp->parent, otherp) == FAILURE) { + php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(this_intern->document)); + return INSERT_ADJACENT_RES_FAILED; + } + if (!php_dom_adopt_node(otherp, this_intern, thisp->doc)) { + return INSERT_ADJACENT_RES_FAILED; + } + otherp = xmlAddNextSibling(thisp, otherp); + } else { + php_dom_throw_error(SYNTAX_ERR, dom_get_strict_error(this_intern->document)); + return INSERT_ADJACENT_RES_FAILED; + } + dom_reconcile_ns(thisp->doc, otherp); + return otherp; +} + +/* {{{ URL: https://dom.spec.whatwg.org/#dom-element-insertadjacentelement +Since: +*/ +PHP_METHOD(DOMElement, insertAdjacentElement) +{ + zend_string *where; + zval *element_zval, *id; + xmlNodePtr thisp, otherp; + dom_object *this_intern, *other_intern; + int ret; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "SO", &where, &element_zval, dom_element_class_entry) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); + DOM_GET_OBJ(otherp, element_zval, xmlNodePtr, other_intern); + + xmlNodePtr result = dom_insert_adjacent(where, thisp, this_intern, otherp); + if (result == NULL) { + RETURN_NULL(); + } else if (result != INSERT_ADJACENT_RES_FAILED) { + DOM_RET_OBJ(otherp, &ret, other_intern); + } else { + RETURN_THROWS(); + } +} +/* }}} end DOMElement::insertAdjacentElement */ + +/* {{{ URL: https://dom.spec.whatwg.org/#dom-element-insertadjacenttext +Since: +*/ +PHP_METHOD(DOMElement, insertAdjacentText) +{ + zend_string *where, *data; + dom_object *this_intern; + zval *id; + xmlNodePtr thisp; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS", &where, &data) == FAILURE) { + RETURN_THROWS(); + } + + DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); + + if (UNEXPECTED(ZEND_SIZE_T_INT_OVFL(ZSTR_LEN(data)))) { + zend_argument_value_error(2, "is too long"); + RETURN_THROWS(); + } + + xmlNodePtr otherp = xmlNewDocTextLen(thisp->doc, (const xmlChar *) ZSTR_VAL(data), ZSTR_LEN(data)); + xmlNodePtr result = dom_insert_adjacent(where, thisp, this_intern, otherp); + if (result == NULL || result == INSERT_ADJACENT_RES_FAILED) { + xmlFreeNode(otherp); + } +} +/* }}} end DOMElement::insertAdjacentText */ + #endif diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 91b51f8a14820..f0a2d598625c3 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -151,6 +151,7 @@ void php_dom_get_content_into_zval(const xmlNode *nodep, zval *target, bool defa zend_string *dom_node_concatenated_name_helper(size_t name_len, const char *name, size_t prefix_len, const char *prefix); 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); /* parentnode */ void dom_parent_node_prepend(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 4b73183e17f50..e02036b586baa 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -661,6 +661,10 @@ public function prepend(...$nodes): void {} /** @param DOMNode|string $nodes */ public function replaceChildren(...$nodes): void {} + + public function insertAdjacentElement(string $where, DOMElement $element): ?DOMElement {} + + public function insertAdjacentText(string $where, string $data): void {} } class DOMDocument extends DOMNode implements DOMParentNode diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index c2e2d8e04049a..796554a189e3f 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: 7070b07b2dee16222242b7e516372a6562d87036 */ + * Stub hash: 850ab297bd3e6162e0497769cace87a41e8e8a00 */ 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) @@ -296,6 +296,16 @@ ZEND_END_ARG_INFO() #define arginfo_class_DOMElement_replaceChildren arginfo_class_DOMParentNode_append +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_DOMElement_insertAdjacentElement, 0, 2, DOMElement, 1) + ZEND_ARG_TYPE_INFO(0, where, IS_STRING, 0) + ZEND_ARG_OBJ_INFO(0, element, DOMElement, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMElement_insertAdjacentText, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, where, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) +ZEND_END_ARG_INFO() + 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, "\"\"") @@ -588,6 +598,8 @@ ZEND_METHOD(DOMElement, replaceWith); ZEND_METHOD(DOMElement, append); ZEND_METHOD(DOMElement, prepend); ZEND_METHOD(DOMElement, replaceChildren); +ZEND_METHOD(DOMElement, insertAdjacentElement); +ZEND_METHOD(DOMElement, insertAdjacentText); ZEND_METHOD(DOMDocument, __construct); ZEND_METHOD(DOMDocument, createAttribute); ZEND_METHOD(DOMDocument, createAttributeNS); @@ -812,6 +824,8 @@ static const zend_function_entry class_DOMElement_methods[] = { 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_ME(DOMElement, insertAdjacentElement, arginfo_class_DOMElement_insertAdjacentElement, ZEND_ACC_PUBLIC) + ZEND_ME(DOMElement, insertAdjacentText, arginfo_class_DOMElement_insertAdjacentText, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/dom/tests/DOMElement_insertAdjacentElement.phpt b/ext/dom/tests/DOMElement_insertAdjacentElement.phpt new file mode 100644 index 0000000000000..1e1eb1efececb --- /dev/null +++ b/ext/dom/tests/DOMElement_insertAdjacentElement.phpt @@ -0,0 +1,128 @@ +--TEST-- +DOMElement::insertAdjacentElement() +--EXTENSIONS-- +dom +--FILE-- +loadXML('

foo

'); +$container = $dom->documentElement; +$p = $container->firstElementChild; + +echo "--- Edge cases ---\n"; + +var_dump($dom->createElement('free')->insertAdjacentElement("beforebegin", $dom->createElement('element'))); +var_dump($dom->createElement('free')->insertAdjacentElement("afterend", $dom->createElement('element'))); + +try { + var_dump($dom->createElement('free')->insertAdjacentElement("bogus", $dom->createElement('element'))); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +echo "--- Hierarchy test ---\n"; + +$element = $dom->createElement('free'); +$child = $element->appendChild($dom->createElement('child')); +foreach (['beforebegin', 'afterbegin', 'beforeend', 'afterend'] as $where) { + try { + var_dump($child->insertAdjacentElement($where, $element)->tagName); + } catch (DOMException $e) { + echo $e->getMessage(), "\n"; + } +} + +function testNormalCases($dom, $uppercase) { + $container = $dom->documentElement; + $p = $container->firstElementChild; + $transform = fn ($s) => $uppercase ? strtoupper($s) : $s; + + var_dump($p->insertAdjacentElement($transform("beforebegin"), $dom->createElement('A'))->tagName); + echo $dom->saveXML(); + + var_dump($p->insertAdjacentElement($transform("afterbegin"), $dom->createElement('B'))->tagName); + echo $dom->saveXML(); + + var_dump($p->insertAdjacentElement($transform("beforeend"), $dom->createElement('C'))->tagName); + echo $dom->saveXML(); + + var_dump($p->insertAdjacentElement($transform("afterend"), $dom->createElement('D'))->tagName); + echo $dom->saveXML(); +} + +echo "--- Normal cases uppercase ---\n"; + +testNormalCases(clone $dom, true); + +echo "--- Normal cases lowercase ---\n"; + +testNormalCases($dom, false); + +$empty = $dom->createElement('empty'); +var_dump($empty->insertAdjacentElement("afterbegin", $dom->createElement('A'))->tagName); +echo $dom->saveXML($empty), "\n"; + +echo "--- Namespace test ---\n"; + +$dom->loadXML(''); +$dom->documentElement->insertAdjacentElement("afterbegin", $dom->createElementNS("some:ns", "bar")); +echo $dom->saveXML(); + +echo "--- Two document test ---\n"; + +$dom1 = new DOMDocument(); +$dom1->loadXML('
'); +$dom2 = new DOMDocument(); +$dom2->loadXML('

'); +$dom1->documentElement->firstChild->insertAdjacentElement('afterbegin', $dom2->documentElement->firstChild); +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +?> +--EXPECT-- +--- Edge cases --- +NULL +NULL +Syntax Error +--- Hierarchy test --- +Hierarchy Request Error +Hierarchy Request Error +Hierarchy Request Error +Hierarchy Request Error +--- Normal cases uppercase --- +string(1) "A" + +

foo

+string(1) "B" + +

foo

+string(1) "C" + +

foo

+string(1) "D" + +

foo

+--- Normal cases lowercase --- +string(1) "A" + +

foo

+string(1) "B" + +

foo

+string(1) "C" + +

foo

+string(1) "D" + +

foo

+string(1) "A" +
+--- Namespace test --- + + +--- Two document test --- + +

+ + diff --git a/ext/dom/tests/DOMElement_insertAdjacentText.phpt b/ext/dom/tests/DOMElement_insertAdjacentText.phpt new file mode 100644 index 0000000000000..58af0812d6fe5 --- /dev/null +++ b/ext/dom/tests/DOMElement_insertAdjacentText.phpt @@ -0,0 +1,81 @@ +--TEST-- +DOMElement::insertAdjacentText() +--EXTENSIONS-- +dom +--FILE-- +loadXML('

foo

'); + +echo "--- Edge cases ---\n"; + +try { + $dom->createElement('free')->insertAdjacentText("bogus", "bogus"); +} catch (DOMException $e) { + echo $e->getMessage(), "\n"; +} + +function testNormalCases($dom, $uppercase) { + $container = $dom->documentElement; + $p = $container->firstElementChild; + $transform = fn ($s) => $uppercase ? strtoupper($s) : $s; + + $p->insertAdjacentText("beforebegin", 'A'); + echo $dom->saveXML(); + + $p->insertAdjacentText("afterbegin", 'B'); + echo $dom->saveXML(); + + $p->insertAdjacentText("beforeend", 'C'); + echo $dom->saveXML(); + + $p->insertAdjacentText("afterend", 'D'); + echo $dom->saveXML(); +} + +echo "--- Normal cases uppercase ---\n"; + +testNormalCases(clone $dom, true); + +echo "--- Normal cases lowercase ---\n"; + +testNormalCases($dom, false); + +echo "--- Normal cases starting from empty element ---\n"; + +$empty = $dom->createElement('empty'); +$empty->insertAdjacentText("afterbegin", 'A'); +echo $dom->saveXML($empty), "\n"; + +$AText = $empty->firstChild; +$empty->insertAdjacentText("afterbegin", 'B'); +echo $dom->saveXML($empty), "\n"; +var_dump($AText->textContent); + +?> +--EXPECT-- +--- Edge cases --- +Syntax Error +--- Normal cases uppercase --- + +A

foo

+ +A

Bfoo

+ +A

BfooC

+ +A

BfooC

D
+--- Normal cases lowercase --- + +A

foo

+ +A

Bfoo

+ +A

BfooC

+ +A

BfooC

D
+--- Normal cases starting from empty element --- +A +BA +string(2) "BA"