[php-src] master: Merge branch 'PHP-8.5'
Ilia Alshanetsky <[email protected]>
| Newsgroups | gmane.comp.php.cvs.general |
|---|---|
| Message-ID | <[email protected]> |
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-30T11:59:03-04:00
Commit: https://github.com/php/php-src/commit/0db31e369ce454cdbb9db746200a17e9d4cdf0f7
Raw diff: https://github.com/php/php-src/commit/0db31e369ce454cdbb9db746200a17e9d4cdf0f7.diff
Merge branch 'PHP-8.5'
* PHP-8.5:
dom: invalidate node list caches on class attribute mutations
Changed paths:
A ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt
M NEWS
M ext/dom/element.c
M ext/dom/token_list.c
Diff:
diff --git a/NEWS b/NEWS
index f79aeae74292..2dd3d1275b5a 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,10 @@ PHP NEWS
. Fixed bug GH-23242 (PHP development server does not support Expect
100-continue flow control). (Sjoerd Langkemper)
+- DOM:
+ . Fixed stale getElementsByClassName() and other node list caches after
+ className/classList writes and attribute removals. (Ilia Alshanetsky)
+
- Intl:
. Fixed a memory leak when dumping IntlCalendar instances. (Ilia Alshanetsky)
. Fixed Collator::sortWithSortKeys() allocating fixed 2MiB buffers
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 354466623ca4..5fcffaf42055 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -154,6 +154,7 @@ static xmlAttrPtr dom_element_reflected_attribute_write(dom_object *obj, zval *n
/* Typed property, so it is a string already */
ZEND_ASSERT(Z_TYPE_P(newval) == IS_STRING);
+ php_libxml_invalidate_node_list_cache(obj->document);
return xmlSetNsProp(nodep, NULL, (const xmlChar *) name, (const xmlChar *) Z_STRVAL_P(newval));
}
@@ -542,7 +543,7 @@ static void dom_deep_ns_redef(xmlNodePtr node, xmlNsPtr ns_to_redefine)
efree(worklist);
}
-static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
+static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp, php_libxml_ref_obj *document)
{
ZEND_ASSERT(thisp != NULL);
ZEND_ASSERT(attrp != NULL);
@@ -597,6 +598,7 @@ static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
return false;
default: ZEND_UNREACHABLE();
}
+ php_libxml_invalidate_node_list_cache(document);
return true;
}
@@ -622,7 +624,7 @@ PHP_METHOD(DOMElement, removeAttribute)
RETURN_FALSE;
}
- RETURN_BOOL(dom_remove_attribute(nodep, attrp));
+ RETURN_BOOL(dom_remove_attribute(nodep, attrp, intern->document));
}
PHP_METHOD(Dom_Element, removeAttribute)
@@ -640,7 +642,7 @@ PHP_METHOD(Dom_Element, removeAttribute)
attrp = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len);
if (attrp != NULL) {
- dom_remove_attribute(nodep, attrp);
+ dom_remove_attribute(nodep, attrp, intern->document);
}
}
/* }}} end dom_element_remove_attribute */
@@ -798,6 +800,7 @@ static void dom_element_remove_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend
RETURN_FALSE;
}
+ php_libxml_invalidate_node_list_cache(intern->document);
xmlUnlinkNode((xmlNodePtr) attrp);
DOM_RET_OBJ((xmlNodePtr) attrp, intern);
@@ -1198,6 +1201,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
if (nsptr != NULL) {
if (xmlStrEqual(BAD_CAST uri, nsptr->href)) {
dom_eliminate_ns(nodep, nsptr);
+ php_libxml_invalidate_node_list_cache(intern->document);
} else {
return;
}
@@ -1212,6 +1216,7 @@ PHP_METHOD(DOMElement, removeAttributeNS)
} else {
xmlUnlinkNode((xmlNodePtr) attrp);
}
+ php_libxml_invalidate_node_list_cache(intern->document);
}
}
/* }}} end dom_element_remove_attribute_ns */
@@ -1947,7 +1952,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
/* Step 5 */
if (force_is_null || !force) {
- retval = !dom_remove_attribute(thisp, attribute);
+ retval = !dom_remove_attribute(thisp, attribute, intern->document);
goto out;
}
diff --git a/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt b/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt
new file mode 100644
index 000000000000..4efdad1b59b4
--- /dev/null
+++ b/ext/dom/tests/modern/common/getElementsByClassName_cache_invalidation.phpt
@@ -0,0 +1,44 @@
+--TEST--
+getElementsByClassName() cache must be invalidated by class attribute mutations
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+function mk($body) {
+ return Dom\HTMLDocument::createFromString("<!DOCTYPE html><html><body>$body</body></html>");
+}
+
+$checks = [
+ 'className' => function ($doc, $span) { $span->className = 'zzz'; },
+ 'classList-remove' => function ($doc, $span) { $span->classList->remove('foo'); },
+ 'classList-value' => function ($doc, $span) { $span->classList->value = 'zzz'; },
+ 'setAttribute' => function ($doc, $span) { $span->setAttribute('class', 'zzz'); },
+ 'removeAttribute' => function ($doc, $span) { $span->removeAttribute('class'); },
+ 'removeAttributeNode' => function ($doc, $span) { $span->removeAttributeNode($span->attributes['class']); },
+];
+foreach ($checks as $label => $fn) {
+ $doc = mk('<span class="foo"></span>');
+ $coll = $doc->getElementsByClassName('foo');
+ if ($coll->length !== 1) {
+ echo "$label: unexpected initial length\n";
+ continue;
+ }
+ $fn($doc, $doc->querySelector('span'));
+ echo "$label: ", $coll->length === 0 ? "OK" : "STALE {$coll->length}", "\n";
+}
+
+$doc = mk('<span></span>');
+$coll = $doc->getElementsByClassName('foo');
+var_dump($coll->length);
+$doc->querySelector('span')->className = 'foo';
+echo $coll->length === 1 ? "growth OK" : "growth STALE", "\n";
+?>
+--EXPECT--
+className: OK
+classList-remove: OK
+classList-value: OK
+setAttribute: OK
+removeAttribute: OK
+removeAttributeNode: OK
+int(0)
+growth OK
diff --git a/ext/dom/token_list.c b/ext/dom/token_list.c
index 0e7797616554..30f308a14d67 100644
--- a/ext/dom/token_list.c
+++ b/ext/dom/token_list.c
@@ -182,6 +182,7 @@ static void dom_token_list_update(dom_token_list_object *intern)
HashTable *token_set = TOKEN_LIST_GET_SET(intern);
php_libxml_invalidate_cache_tag(&intern->cache_tag);
+ php_libxml_invalidate_node_list_cache(intern->dom.document);
/* 1. If the associated element does not have an associated attribute and token set is empty, then return. */
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)
zend_value_error("Value must not contain any null bytes");
return FAILURE;
}
+ php_libxml_invalidate_node_list_cache(intern->dom.document);
xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST Z_STRVAL_P(newval));
/* Note: we don't update the set here, the set is always lazily updated for performance reasons. */
return SUCCESS;