Skip to content

Commit 0db31e3

Browse files
committed
Merge branch 'PHP-8.5'
* PHP-8.5: dom: invalidate node list caches on class attribute mutations
2 parents a738269 + fbca6cb commit 0db31e3

4 files changed

Lines changed: 59 additions & 4 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ PHP NEWS
66
. Fixed bug GH-23242 (PHP development server does not support Expect
77
100-continue flow control). (Sjoerd Langkemper)
88

9+
- DOM:
10+
. Fixed stale getElementsByClassName() and other node list caches after
11+
className/classList writes and attribute removals. (Ilia Alshanetsky)
12+
913
- Intl:
1014
. Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky)
1115
. Fixed Collator::sortWithSortKeys() allocating fixed 2MiB buffers

ext/dom/element.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ static xmlAttrPtr dom_element_reflected_attribute_write(dom_object *obj, zval *n
154154

155155
/* Typed property, so it is a string already */
156156
ZEND_ASSERT(Z_TYPE_P(newval) == IS_STRING);
157+
php_libxml_invalidate_node_list_cache(obj->document);
157158
return xmlSetNsProp(nodep, NULL, (const xmlChar *) name, (const xmlChar *) Z_STRVAL_P(newval));
158159
}
159160

@@ -542,7 +543,7 @@ static void dom_deep_ns_redef(xmlNodePtr node, xmlNsPtr ns_to_redefine)
542543
efree(worklist);
543544
}
544545

545-
static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
546+
static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp, php_libxml_ref_obj *document)
546547
{
547548
ZEND_ASSERT(thisp != NULL);
548549
ZEND_ASSERT(attrp != NULL);
@@ -597,6 +598,7 @@ static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
597598
return false;
598599
default: ZEND_UNREACHABLE();
599600
}
601+
php_libxml_invalidate_node_list_cache(document);
600602
return true;
601603
}
602604

@@ -622,7 +624,7 @@ PHP_METHOD(DOMElement, removeAttribute)
622624
RETURN_FALSE;
623625
}
624626

625-
RETURN_BOOL(dom_remove_attribute(nodep, attrp));
627+
RETURN_BOOL(dom_remove_attribute(nodep, attrp, intern->document));
626628
}
627629

628630
PHP_METHOD(Dom_Element, removeAttribute)
@@ -640,7 +642,7 @@ PHP_METHOD(Dom_Element, removeAttribute)
640642

641643
attrp = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len);
642644
if (attrp != NULL) {
643-
dom_remove_attribute(nodep, attrp);
645+
dom_remove_attribute(nodep, attrp, intern->document);
644646
}
645647
}
646648
/* }}} end dom_element_remove_attribute */
@@ -798,6 +800,7 @@ static void dom_element_remove_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend
798800
RETURN_FALSE;
799801
}
800802

803+
php_libxml_invalidate_node_list_cache(intern->document);
801804
xmlUnlinkNode((xmlNodePtr) attrp);
802805

803806
DOM_RET_OBJ((xmlNodePtr) attrp, intern);
@@ -1198,6 +1201,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
11981201
if (nsptr != NULL) {
11991202
if (xmlStrEqual(BAD_CAST uri, nsptr->href)) {
12001203
dom_eliminate_ns(nodep, nsptr);
1204+
php_libxml_invalidate_node_list_cache(intern->document);
12011205
} else {
12021206
return;
12031207
}
@@ -1212,6 +1216,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
12121216
} else {
12131217
xmlUnlinkNode((xmlNodePtr) attrp);
12141218
}
1219+
php_libxml_invalidate_node_list_cache(intern->document);
12151220
}
12161221
}
12171222
/* }}} end dom_element_remove_attribute_ns */
@@ -1947,7 +1952,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
19471952

19481953
/* Step 5 */
19491954
if (force_is_null || !force) {
1950-
retval = !dom_remove_attribute(thisp, attribute);
1955+
retval = !dom_remove_attribute(thisp, attribute, intern->document);
19511956
goto out;
19521957
}
19531958

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
getElementsByClassName() cache must be invalidated by class attribute mutations
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
function mk($body) {
8+
return Dom\HTMLDocument::createFromString("<!DOCTYPE html><html><body>$body</body></html>");
9+
}
10+
11+
$checks = [
12+
'className' => function ($doc, $span) { $span->className = 'zzz'; },
13+
'classList-remove' => function ($doc, $span) { $span->classList->remove('foo'); },
14+
'classList-value' => function ($doc, $span) { $span->classList->value = 'zzz'; },
15+
'setAttribute' => function ($doc, $span) { $span->setAttribute('class', 'zzz'); },
16+
'removeAttribute' => function ($doc, $span) { $span->removeAttribute('class'); },
17+
'removeAttributeNode' => function ($doc, $span) { $span->removeAttributeNode($span->attributes['class']); },
18+
];
19+
foreach ($checks as $label => $fn) {
20+
$doc = mk('<span class="foo"></span>');
21+
$coll = $doc->getElementsByClassName('foo');
22+
if ($coll->length !== 1) {
23+
echo "$label: unexpected initial length\n";
24+
continue;
25+
}
26+
$fn($doc, $doc->querySelector('span'));
27+
echo "$label: ", $coll->length === 0 ? "OK" : "STALE {$coll->length}", "\n";
28+
}
29+
30+
$doc = mk('<span></span>');
31+
$coll = $doc->getElementsByClassName('foo');
32+
var_dump($coll->length);
33+
$doc->querySelector('span')->className = 'foo';
34+
echo $coll->length === 1 ? "growth OK" : "growth STALE", "\n";
35+
?>
36+
--EXPECT--
37+
className: OK
38+
classList-remove: OK
39+
classList-value: OK
40+
setAttribute: OK
41+
removeAttribute: OK
42+
removeAttributeNode: OK
43+
int(0)
44+
growth OK

ext/dom/token_list.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ static void dom_token_list_update(dom_token_list_object *intern)
182182
HashTable *token_set = TOKEN_LIST_GET_SET(intern);
183183

184184
php_libxml_invalidate_cache_tag(&intern->cache_tag);
185+
php_libxml_invalidate_node_list_cache(intern->dom.document);
185186

186187
/* 1. If the associated element does not have an associated attribute and token set is empty, then return. */
187188
if (attr == NULL && zend_hash_num_elements(token_set) == 0) {
@@ -430,6 +431,7 @@ zend_result dom_token_list_value_write(dom_object *obj, zval *newval)
430431
zend_value_error("Value must not contain any null bytes");
431432
return FAILURE;
432433
}
434+
php_libxml_invalidate_node_list_cache(intern->dom.document);
433435
xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST Z_STRVAL_P(newval));
434436
/* Note: we don't update the set here, the set is always lazily updated for performance reasons. */
435437
return SUCCESS;

0 commit comments

Comments
 (0)