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 ---
+
+Afoo
+
+ABfoo
+
+ABfooC
+
+ABfooC
D
+--- Normal cases lowercase ---
+
+Afoo
+
+ABfoo
+
+ABfooC
+
+ABfooC
D
+--- Normal cases starting from empty element ---
+A
+BA
+string(2) "BA"