Skip to content

Commit 0a5edc5

Browse files
committed
Merge branch 'PHP-8.5'
* PHP-8.5: ext/dom: getElementsByClassName() item() returns wrong element on random access.
2 parents f36660d + 7f97b83 commit 0a5edc5

2 files changed

Lines changed: 91 additions & 9 deletions

File tree

ext/dom/obj_map.c

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,22 +345,20 @@ static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long i
345345
if (nodep && index >= 0) {
346346
dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index);
347347
if (start_point.node) {
348-
if (start_point.index > 0) {
349-
/* Only start iteration at next point if we actually have an index to seek to. */
350-
itemnode = php_dom_next_in_tree_order(start_point.node, nodep);
351-
} else {
352-
itemnode = start_point.node;
353-
}
348+
itemnode = start_point.node;
354349
} else {
355350
itemnode = php_dom_first_child_of_container_node(nodep);
351+
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
352+
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
353+
}
356354
}
357355

358-
do {
359-
--start_point.index;
356+
for (; start_point.index > 0 && itemnode != NULL; --start_point.index) {
357+
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
360358
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
361359
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
362360
}
363-
} while (start_point.index > 0 && itemnode);
361+
}
364362
}
365363
dom_ret_node_to_zobj(map, itemnode, return_value);
366364
if (itemnode) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--TEST--
2+
Dom\Element::getElementsByClassName() item() random access (cold and backwards)
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
/* Regression: the item() lookup must return the n-th match for random access,
9+
* not only for a strictly ascending / foreach walk. A fresh collection whose
10+
* first access is item(n) exercises the uncached path, and a decreasing index
11+
* on the same collection exercises the cache-discard path. Non-matching and
12+
* nested elements verify that iteration advances to the next matching element
13+
* in tree order, not merely to the next sibling. */
14+
15+
$dom = Dom\HTMLDocument::createFromString(<<<HTML
16+
<!DOCTYPE html>
17+
<body>
18+
<span class="x" id="E0"></span>
19+
<div class="y" id="skip1"><span class="x" id="E1"></span></div>
20+
<span class="x" id="E2"></span>
21+
<p class="z"><b class="x" id="E3"></b></p>
22+
<span class="x" id="E4"></span>
23+
</body>
24+
HTML);
25+
26+
$body = $dom->getElementsByTagName('body')->item(0);
27+
28+
function id(?Dom\Element $e): string {
29+
return $e === null ? 'NULL' : $e->id;
30+
}
31+
32+
echo "-- cold random access (fresh collection per call) --\n";
33+
foreach ([0, 1, 2, 3, 4, 5] as $i) {
34+
$collection = $body->getElementsByClassName('x');
35+
echo "item($i) = ", id($collection->item($i)), "\n";
36+
}
37+
38+
echo "-- backwards seek on one collection --\n";
39+
$collection = $body->getElementsByClassName('x');
40+
foreach ([4, 2, 0, 3, 1] as $i) {
41+
echo "item($i) = ", id($collection->item($i)), "\n";
42+
}
43+
44+
echo "-- item() seed then foreach --\n";
45+
$collection = $body->getElementsByClassName('x');
46+
$collection->item(3);
47+
$ids = [];
48+
foreach ($collection as $node) {
49+
$ids[] = $node->id;
50+
}
51+
echo implode(" ", $ids), "\n";
52+
53+
echo "-- last-element idiom --\n";
54+
$collection = $body->getElementsByClassName('x');
55+
echo "length = ", $collection->length, ", last = ", id($collection->item($collection->length - 1)), "\n";
56+
57+
echo "-- live collection after mutation --\n";
58+
$collection = $body->getElementsByClassName('x');
59+
echo "item(1) = ", id($collection->item(1)), "\n";
60+
$dom->getElementById('E1')->remove();
61+
echo "item(1) = ", id($collection->item(1)), ", length = ", $collection->length, "\n";
62+
63+
?>
64+
--EXPECT--
65+
-- cold random access (fresh collection per call) --
66+
item(0) = E0
67+
item(1) = E1
68+
item(2) = E2
69+
item(3) = E3
70+
item(4) = E4
71+
item(5) = NULL
72+
-- backwards seek on one collection --
73+
item(4) = E4
74+
item(2) = E2
75+
item(0) = E0
76+
item(3) = E3
77+
item(1) = E1
78+
-- item() seed then foreach --
79+
E0 E1 E2 E3 E4
80+
-- last-element idiom --
81+
length = 5, last = E4
82+
-- live collection after mutation --
83+
item(1) = E1
84+
item(1) = E2, length = 4

0 commit comments

Comments
 (0)