Skip to content

Commit

Permalink
Fix GH-11625: DOMElement::replaceWith() doesn't replace node with DOM…
Browse files Browse the repository at this point in the history
…DocumentFragment but just deletes node or causes wrapping <></> depending on libxml2 version

Depending on the libxml2 version, the behaviour is either to not
render the fragment correctly, or to wrap it inside <></>. Fix it by
unpacking fragments manually. This has the side effect that we need to
move the unlinking check in the replacement function to earlier because
the empty child list is now possible in non-error cases.
Also fixes a mistake in the linked list management.

Closes GH-11627.
  • Loading branch information
nielsdos committed Jul 10, 2023
1 parent 0d07b6d commit 15ff830
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 7 deletions.
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ PHP NEWS
- Date:
. Fixed bug GH-11368 (Date modify returns invalid datetime). (Derick)

- DOM:
. Fixed bug GH-11625 (DOMElement::replaceWith() doesn't replace node with
DOMDocumentFragment but just deletes node or causes wrapping <></>
depending on libxml2 version). (nielsdos)

- Fileinfo:
. Fixed bug GH-11298 (finfo returns wrong mime type for xz files). (Anatol)

Expand Down
22 changes: 15 additions & 7 deletions ext/dom/parentnode.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,15 @@ xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNod
goto err;
}

if (newNode->parent != NULL) {
if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
/* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
newNode = newNode->children;
if (UNEXPECTED(newNode == NULL)) {
/* No nodes to add, nothing to do here */
continue;
}
xmlUnlinkNode(newNode);
} else if (newNode->parent != NULL) {
xmlUnlinkNode(newNode);
}

Expand Down Expand Up @@ -370,7 +378,7 @@ static void dom_pre_insert(xmlNodePtr insertion_point, xmlNodePtr parentNode, xm
insertion_point->prev->next = newchild;
newchild->prev = insertion_point->prev;
}
insertion_point->prev = newchild;
insertion_point->prev = fragment->last;
if (parentNode->children == insertion_point) {
parentNode->children = newchild;
}
Expand Down Expand Up @@ -555,14 +563,14 @@ void dom_child_replace_with(dom_object *context, zval *nodes, int nodesc)
xmlNodePtr newchild = fragment->children;
xmlDocPtr doc = parentNode->doc;

/* Unlink and free it unless it became a part of the fragment. */
if (child->parent != fragment) {
xmlUnlinkNode(child);
}

if (newchild) {
xmlNodePtr last = fragment->last;

/* Unlink and free it unless it became a part of the fragment. */
if (child->parent != fragment) {
xmlUnlinkNode(child);
}

dom_pre_insert(insertion_point, parentNode, newchild, fragment);

dom_fragment_assign_parent_node(parentNode, fragment);
Expand Down
72 changes: 72 additions & 0 deletions ext/dom/tests/gh11625.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
--TEST--
GH-11625 (DOMElement::replaceWith() doesn't replace node with DOMDocumentFragment but just deletes node or causes wrapping <></> depending on libxml2 version)
--EXTENSIONS--
dom
--FILE--
<?php

function test($mutator) {
$html = <<<XML
<body>
<div></div><div></div>
</body>
XML;

$dom = new DOMDocument();
$dom->loadXML($html);

$divs = iterator_to_array($dom->getElementsByTagName('div')->getIterator());
$i = 0;
foreach ($divs as $div) {
$mutator($dom, $div, $i);
echo $dom->saveHTML();
$i++;
}
}

echo "--- Single replacement ---\n";

test(function($dom, $div, $i) {
$fragment = $dom->createDocumentFragment();
$fragment->appendXML("<p>Hi $i!</p>");
$div->replaceWith($fragment);
});

echo "--- Multiple replacement ---\n";

test(function($dom, $div, $i) {
$fragment = $dom->createDocumentFragment();
$fragment->appendXML("<p>Hi $i!</p>");
$div->replaceWith($fragment, $dom->createElement('x'), "hello");
});

echo "--- Empty fragment replacement ---\n";

test(function($dom, $div, $i) {
$fragment = $dom->createDocumentFragment();
$div->replaceWith($fragment);
});

?>
--EXPECT--
--- Single replacement ---
<body>
<p>Hi 0!</p><div></div>
</body>
<body>
<p>Hi 0!</p><p>Hi 1!</p>
</body>
--- Multiple replacement ---
<body>
<p>Hi 0!</p><x></x>hello<div></div>
</body>
<body>
<p>Hi 0!</p><x></x>hello<p>Hi 1!</p><x></x>hello
</body>
--- Empty fragment replacement ---
<body>
<div></div>
</body>
<body>

</body>

0 comments on commit 15ff830

Please sign in to comment.