Skip to content

Implement DOMElement::insertAdjacent{Element,Text} #11700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 22 additions & 15 deletions ext/dom/document.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
116 changes: 116 additions & 0 deletions ext/dom/element.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +1399 to +1400
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec seem to indicate that a DOMException must be thrown:

Suggested change
php_dom_throw_error(SYNTAX_ERR, dom_get_strict_error(this_intern->document));
return INSERT_ADJACENT_RES_FAILED;
php_dom_throw_error(SYNTAX_ERR, true);
return INSERT_ADJACENT_RES_FAILED;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but strangely PHP offers an "stricterror" option (default true) which allows you to get warnings instead of exceptions.
And that maybe doesn't even violate spec, because from https://webidl.spec.whatwg.org/#dfn-throw

The resulting behavior from creating and throwing an exception is language binding specific.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah PHP extension used to have the trifecto of no warning/exception, warnings, exceptions. I think mainly because prior to the introduction of exception warnings were the only way.

We probably should get rid of those and only allow silent or exception behaviour (similarly to the SQLite3 RFC) but that's future scope

}
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
1 change: 1 addition & 0 deletions ext/dom/php_dom.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions ext/dom/php_dom.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion ext/dom/php_dom_arginfo.h

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

128 changes: 128 additions & 0 deletions ext/dom/tests/DOMElement_insertAdjacentElement.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
--TEST--
DOMElement::insertAdjacentElement()
--EXTENSIONS--
dom
--FILE--
<?php

$dom = new DOMDocument();
$dom->loadXML('<?xml version="1.0"?><container><p>foo</p></container>');
$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('<?xml version="1.0"?><container xmlns:foo="some:ns"/>');
$dom->documentElement->insertAdjacentElement("afterbegin", $dom->createElementNS("some:ns", "bar"));
echo $dom->saveXML();

echo "--- Two document test ---\n";

$dom1 = new DOMDocument();
$dom1->loadXML('<?xml version="1.0"?><container><div/></container>');
$dom2 = new DOMDocument();
$dom2->loadXML('<?xml version="1.0"?><container><p/></container>');
$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"
<?xml version="1.0"?>
<container><A/><p>foo</p></container>
string(1) "B"
<?xml version="1.0"?>
<container><A/><p><B/>foo</p></container>
string(1) "C"
<?xml version="1.0"?>
<container><A/><p><B/>foo<C/></p></container>
string(1) "D"
<?xml version="1.0"?>
<container><A/><p><B/>foo<C/></p><D/></container>
--- Normal cases lowercase ---
string(1) "A"
<?xml version="1.0"?>
<container><A/><p>foo</p></container>
string(1) "B"
<?xml version="1.0"?>
<container><A/><p><B/>foo</p></container>
string(1) "C"
<?xml version="1.0"?>
<container><A/><p><B/>foo<C/></p></container>
string(1) "D"
<?xml version="1.0"?>
<container><A/><p><B/>foo<C/></p><D/></container>
string(1) "A"
<empty><A/></empty>
--- Namespace test ---
<?xml version="1.0"?>
<container xmlns:foo="some:ns"><foo:bar/></container>
--- Two document test ---
<?xml version="1.0"?>
<container><div><p/></div></container>
<?xml version="1.0"?>
<container/>
Loading