Skip to content

Commit f397ad0

Browse files
committed
Fix phpGH-11625: DOMElement::replaceWith() doesn't replace node with DOMDocumentFragment 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.
1 parent bbe72f1 commit f397ad0

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

ext/dom/parentnode.c

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,15 @@ xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNod
183183
goto err;
184184
}
185185

186-
if (newNode->parent != NULL) {
186+
if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
187+
/* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
188+
newNode = newNode->children;
189+
if (UNEXPECTED(newNode == NULL)) {
190+
/* No nodes to add, nothing to do here */
191+
continue;
192+
}
193+
xmlUnlinkNode(newNode);
194+
} else if (newNode->parent != NULL) {
187195
xmlUnlinkNode(newNode);
188196
}
189197

@@ -370,7 +378,7 @@ static void dom_pre_insert(xmlNodePtr insertion_point, xmlNodePtr parentNode, xm
370378
insertion_point->prev->next = newchild;
371379
newchild->prev = insertion_point->prev;
372380
}
373-
insertion_point->prev = newchild;
381+
insertion_point->prev = fragment->last;
374382
if (parentNode->children == insertion_point) {
375383
parentNode->children = newchild;
376384
}
@@ -555,14 +563,14 @@ void dom_child_replace_with(dom_object *context, zval *nodes, int nodesc)
555563
xmlNodePtr newchild = fragment->children;
556564
xmlDocPtr doc = parentNode->doc;
557565

566+
/* Unlink and free it unless it became a part of the fragment. */
567+
if (child->parent != fragment) {
568+
xmlUnlinkNode(child);
569+
}
570+
558571
if (newchild) {
559572
xmlNodePtr last = fragment->last;
560573

561-
/* Unlink and free it unless it became a part of the fragment. */
562-
if (child->parent != fragment) {
563-
xmlUnlinkNode(child);
564-
}
565-
566574
dom_pre_insert(insertion_point, parentNode, newchild, fragment);
567575

568576
dom_fragment_assign_parent_node(parentNode, fragment);

ext/dom/tests/gh11625.phpt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
GH-11625 (DOMElement::replaceWith() doesn't replace node with DOMDocumentFragment but just deletes node or causes wrapping <></> depending on libxml2 version)
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
function test($mutator) {
9+
$html = <<<XML
10+
<body>
11+
<div></div><div></div>
12+
</body>
13+
XML;
14+
15+
$dom = new DOMDocument();
16+
$dom->loadXML($html);
17+
18+
$divs = iterator_to_array($dom->getElementsByTagName('div')->getIterator());
19+
$i = 0;
20+
foreach ($divs as $div) {
21+
$mutator($dom, $div, $i);
22+
echo $dom->saveHTML();
23+
$i++;
24+
}
25+
}
26+
27+
echo "--- Single replacement ---\n";
28+
29+
test(function($dom, $div, $i) {
30+
$fragment = $dom->createDocumentFragment();
31+
$fragment->appendXML("<p>Hi $i!</p>");
32+
$div->replaceWith($fragment);
33+
});
34+
35+
echo "--- Multiple replacement ---\n";
36+
37+
test(function($dom, $div, $i) {
38+
$fragment = $dom->createDocumentFragment();
39+
$fragment->appendXML("<p>Hi $i!</p>");
40+
$div->replaceWith($fragment, $dom->createElement('x'), "hello");
41+
});
42+
43+
echo "--- Empty fragment replacement ---\n";
44+
45+
test(function($dom, $div, $i) {
46+
$fragment = $dom->createDocumentFragment();
47+
$div->replaceWith($fragment);
48+
});
49+
50+
?>
51+
--EXPECT--
52+
--- Single replacement ---
53+
<body>
54+
<p>Hi 0!</p><div></div>
55+
</body>
56+
<body>
57+
<p>Hi 0!</p><p>Hi 1!</p>
58+
</body>
59+
--- Multiple replacement ---
60+
<body>
61+
<p>Hi 0!</p><x></x>hello<div></div>
62+
</body>
63+
<body>
64+
<p>Hi 0!</p><x></x>hello<p>Hi 1!</p><x></x>hello
65+
</body>
66+
--- Empty fragment replacement ---
67+
<body>
68+
<div></div>
69+
</body>
70+
<body>
71+
72+
</body>

0 commit comments

Comments
 (0)