diff --git a/ext/dom/config.m4 b/ext/dom/config.m4
index e67d8dcbfe9c..f16804d468ba 100644
--- a/ext/dom/config.m4
+++ b/ext/dom/config.m4
@@ -36,6 +36,7 @@ if test "$PHP_DOM" != "no"; then
documenttype.c entity.c \
nodelist.c html_collection.c text.c comment.c \
entityreference.c \
+ token_list.c \
notation.c xpath.c dom_iterators.c \
namednodemap.c xpath_callbacks.c \
$LEXBOR_SOURCES],
diff --git a/ext/dom/config.w32 b/ext/dom/config.w32
index 16fe4b253061..081190a67f04 100644
--- a/ext/dom/config.w32
+++ b/ext/dom/config.w32
@@ -14,6 +14,7 @@ if (PHP_DOM == "yes") {
node.c characterdata.c documenttype.c \
entity.c nodelist.c html_collection.c text.c comment.c \
entityreference.c \
+ token_list.c \
notation.c xpath.c dom_iterators.c \
namednodemap.c xpath_callbacks.c", null, "-Iext/dom/lexbor");
diff --git a/ext/dom/dom_ce.h b/ext/dom/dom_ce.h
index 13e58957df31..04870ed95510 100644
--- a/ext/dom/dom_ce.h
+++ b/ext/dom/dom_ce.h
@@ -62,6 +62,7 @@ extern PHP_DOM_EXPORT zend_class_entry *dom_modern_entityreference_class_entry;
extern PHP_DOM_EXPORT zend_class_entry *dom_processinginstruction_class_entry;
extern PHP_DOM_EXPORT zend_class_entry *dom_modern_processinginstruction_class_entry;
extern PHP_DOM_EXPORT zend_class_entry *dom_abstract_base_document_class_entry;
+extern PHP_DOM_EXPORT zend_class_entry *dom_token_list_class_entry;
#ifdef LIBXML_XPATH_ENABLED
extern PHP_DOM_EXPORT zend_class_entry *dom_xpath_class_entry;
extern PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry;
diff --git a/ext/dom/dom_properties.h b/ext/dom/dom_properties.h
index eb0bdb96f738..1c9d0c6e34ad 100644
--- a/ext/dom/dom_properties.h
+++ b/ext/dom/dom_properties.h
@@ -85,6 +85,7 @@ zend_result dom_element_id_write(dom_object *obj, zval *newval);
zend_result dom_element_schema_type_info_read(dom_object *obj, zval *retval);
zend_result dom_element_inner_html_read(dom_object *obj, zval *retval);
zend_result dom_element_inner_html_write(dom_object *obj, zval *newval);
+zend_result dom_element_class_list_read(dom_object *obj, zval *retval);
/* entity properties */
zend_result dom_entity_public_id_read(dom_object *obj, zval *retval);
@@ -148,6 +149,11 @@ zend_result dom_processinginstruction_data_write(dom_object *obj, zval *newval);
/* text properties */
zend_result dom_text_whole_text_read(dom_object *obj, zval *retval);
+/* token_list properties */
+zend_result dom_token_list_length_read(dom_object *obj, zval *retval);
+zend_result dom_token_list_value_read(dom_object *obj, zval *retval);
+zend_result dom_token_list_value_write(dom_object *obj, zval *newval);
+
#ifdef LIBXML_XPATH_ENABLED
/* xpath properties */
zend_result dom_xpath_document_read(dom_object *obj, zval *retval);
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 99043b7f3829..bc375562478b 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -26,6 +26,7 @@
#include "namespace_compat.h"
#include "internal_helpers.h"
#include "dom_properties.h"
+#include "token_list.h"
/*
* class DOMElement extends DOMNode
@@ -175,6 +176,33 @@ zend_result dom_element_class_name_write(dom_object *obj, zval *newval)
}
/* }}} */
+/* {{{ classList TokenList
+URL: https://dom.spec.whatwg.org/#dom-element-classlist
+*/
+zend_result dom_element_class_list_read(dom_object *obj, zval *retval)
+{
+ const uint32_t PROP_INDEX = 20;
+
+#if ZEND_DEBUG
+ zend_string *class_list_str = ZSTR_INIT_LITERAL("classList", false);
+ const zend_property_info *prop_info = zend_get_property_info(dom_modern_element_class_entry, class_list_str, 0);
+ zend_string_release_ex(class_list_str, false);
+ ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == PROP_INDEX);
+#endif
+
+ zval *cached_token_list = OBJ_PROP_NUM(&obj->std, PROP_INDEX);
+ if (Z_ISUNDEF_P(cached_token_list)) {
+ object_init_ex(cached_token_list, dom_token_list_class_entry);
+ dom_token_list_object *intern = php_dom_token_list_from_obj(Z_OBJ_P(cached_token_list));
+ dom_token_list_ctor(intern, obj);
+ }
+
+ ZVAL_OBJ_COPY(retval, Z_OBJ_P(cached_token_list));
+
+ return SUCCESS;
+}
+/* }}} */
+
/* {{{ id string
URL: https://dom.spec.whatwg.org/#dom-element-id
Since:
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index c43f617c35ce..71674f9715bf 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -30,6 +30,7 @@
#include "internal_helpers.h"
#include "php_dom_arginfo.h"
#include "dom_properties.h"
+#include "token_list.h"
#include "zend_interfaces.h"
#include "lexbor/lexbor/core/types.h"
#include "lexbor/lexbor/core/lexbor.h"
@@ -81,6 +82,7 @@ PHP_DOM_EXPORT zend_class_entry *dom_modern_entityreference_class_entry;
PHP_DOM_EXPORT zend_class_entry *dom_processinginstruction_class_entry;
PHP_DOM_EXPORT zend_class_entry *dom_modern_processinginstruction_class_entry;
PHP_DOM_EXPORT zend_class_entry *dom_abstract_base_document_class_entry;
+PHP_DOM_EXPORT zend_class_entry *dom_token_list_class_entry;
#ifdef LIBXML_XPATH_ENABLED
PHP_DOM_EXPORT zend_class_entry *dom_xpath_class_entry;
PHP_DOM_EXPORT zend_class_entry *dom_modern_xpath_class_entry;
@@ -97,6 +99,7 @@ static zend_object_handlers dom_modern_nodelist_object_handlers;
static zend_object_handlers dom_html_collection_object_handlers;
static zend_object_handlers dom_object_namespace_node_handlers;
static zend_object_handlers dom_modern_domimplementation_object_handlers;
+static zend_object_handlers dom_token_list_object_handlers;
#ifdef LIBXML_XPATH_ENABLED
zend_object_handlers dom_xpath_object_handlers;
#endif
@@ -132,6 +135,7 @@ static HashTable dom_modern_entity_prop_handlers;
static HashTable dom_processinginstruction_prop_handlers;
static HashTable dom_modern_processinginstruction_prop_handlers;
static HashTable dom_namespace_node_prop_handlers;
+static HashTable dom_token_list_prop_handlers;
#ifdef LIBXML_XPATH_ENABLED
static HashTable dom_xpath_prop_handlers;
#endif
@@ -633,6 +637,18 @@ static zend_object *dom_object_namespace_node_clone_obj(zend_object *zobject)
return clone;
}
+static zend_object *dom_token_list_new(zend_class_entry *class_type)
+{
+ dom_token_list_object *intern = zend_object_alloc(sizeof(*intern), class_type);
+
+ intern->dom.prop_handler = &dom_token_list_prop_handlers;
+
+ zend_object_std_init(&intern->dom.std, class_type);
+ object_properties_init(&intern->dom.std, class_type);
+
+ return &intern->dom.std;
+}
+
static const zend_module_dep dom_deps[] = {
ZEND_MOD_REQUIRED("libxml")
ZEND_MOD_CONFLICTS("domxml")
@@ -658,7 +674,6 @@ zend_module_entry dom_module_entry = { /* {{{ */
ZEND_GET_MODULE(dom)
#endif
-void dom_objects_free_storage(zend_object *object);
void dom_nnodemap_objects_free_storage(zend_object *object);
static zval *dom_nodelist_read_dimension(zend_object *object, zval *offset, int type, zval *rv);
static int dom_nodelist_has_dimension(zend_object *object, zval *member, int check_empty);
@@ -732,6 +747,16 @@ PHP_MINIT_FUNCTION(dom)
dom_object_namespace_node_handlers.free_obj = dom_object_namespace_node_free_storage;
dom_object_namespace_node_handlers.clone_obj = dom_object_namespace_node_clone_obj;
+ memcpy(&dom_token_list_object_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
+ dom_token_list_object_handlers.offset = XtOffsetOf(dom_token_list_object, dom.std);
+ dom_token_list_object_handlers.free_obj = dom_token_list_free_obj;
+ /* The Web IDL (Web Interface Description Language - https://webidl.spec.whatwg.org) has the [SameObject] constraint
+ * for this object, which is incompatible with cloning because it imposes that there is only one instance
+ * per parent object. */
+ dom_token_list_object_handlers.clone_obj = NULL;
+ dom_token_list_object_handlers.read_dimension = dom_token_list_read_dimension;
+ dom_token_list_object_handlers.has_dimension = dom_token_list_has_dimension;
+
zend_hash_init(&classes, 0, NULL, NULL, true);
dom_adjacent_position_class_entry = register_class_Dom_AdjacentPosition();
@@ -1033,6 +1058,7 @@ PHP_MINIT_FUNCTION(dom)
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "tagName", dom_element_tag_name_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "id", dom_element_id_read, dom_element_id_write);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "className", dom_element_class_name_read, dom_element_class_name_write);
+ DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "classList", dom_element_class_list_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "attributes", dom_node_attributes_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL);
@@ -1227,6 +1253,16 @@ PHP_MINIT_FUNCTION(dom)
zend_hash_add_new_ptr(&classes, dom_modern_xpath_class_entry->name, &dom_xpath_prop_handlers);
#endif
+ dom_token_list_class_entry = register_class_Dom_TokenList(zend_ce_aggregate, zend_ce_countable);
+ dom_token_list_class_entry->create_object = dom_token_list_new;
+ dom_token_list_class_entry->default_object_handlers = &dom_token_list_object_handlers;
+ dom_token_list_class_entry->get_iterator = dom_token_list_get_iterator;
+
+ zend_hash_init(&dom_token_list_prop_handlers, 0, NULL, NULL, true);
+ DOM_REGISTER_PROP_HANDLER(&dom_token_list_prop_handlers, "length", dom_token_list_length_read, NULL);
+ DOM_REGISTER_PROP_HANDLER(&dom_token_list_prop_handlers, "value", dom_token_list_value_read, dom_token_list_value_write);
+ zend_hash_add_new_ptr(&classes, dom_token_list_class_entry->name, &dom_token_list_prop_handlers);
+
register_php_dom_symbols(module_number);
php_libxml_register_export(dom_node_class_entry, php_dom_export_node);
@@ -1292,6 +1328,7 @@ PHP_MSHUTDOWN_FUNCTION(dom) /* {{{ */
zend_hash_destroy(&dom_modern_entity_prop_handlers);
zend_hash_destroy(&dom_processinginstruction_prop_handlers);
zend_hash_destroy(&dom_modern_processinginstruction_prop_handlers);
+ zend_hash_destroy(&dom_token_list_prop_handlers);
#ifdef LIBXML_XPATH_ENABLED
zend_hash_destroy(&dom_xpath_prop_handlers);
#endif
diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h
index cf0667e0692a..d3f099cde218 100644
--- a/ext/dom/php_dom.h
+++ b/ext/dom/php_dom.h
@@ -122,6 +122,7 @@ static inline dom_object_namespace_node *php_dom_namespace_node_obj_from_obj(zen
#define DOM_HTML_NO_DEFAULT_NS (1U << 31)
+void dom_objects_free_storage(zend_object *object);
dom_doc_propsptr dom_get_doc_props(php_libxml_ref_obj *document);
libxml_doc_props const* dom_get_doc_props_read_only(const php_libxml_ref_obj *document);
zend_object *dom_objects_new(zend_class_entry *class_type);
@@ -230,14 +231,8 @@ xmlNodePtr dom_clone_node(php_dom_libxml_ns_mapper *ns_mapper, xmlNodePtr node,
static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php_libxml_cache_tag *cache_tag, const php_libxml_ref_obj *doc_ptr)
{
- ZEND_ASSERT(cache_tag != NULL);
ZEND_ASSERT(doc_ptr != NULL);
- /* See overflow comment in php_libxml_invalidate_node_list_cache(). */
-#if SIZEOF_SIZE_T == 8
- return cache_tag->modification_nr != doc_ptr->cache_tag.modification_nr;
-#else
- return cache_tag->modification_nr != doc_ptr->cache_tag.modification_nr || UNEXPECTED(doc_ptr->cache_tag.modification_nr == SIZE_MAX);
-#endif
+ return php_libxml_is_cache_tag_stale(cache_tag, &doc_ptr->cache_tag);
}
static zend_always_inline bool php_dom_is_cache_tag_stale_from_node(const php_libxml_cache_tag *cache_tag, const xmlNodePtr node)
diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php
index a2f1d8991dde..162355cb232c 100644
--- a/ext/dom/php_dom.stub.php
+++ b/ext/dom/php_dom.stub.php
@@ -1304,6 +1304,8 @@ class Element extends Node implements ParentNode, ChildNode
public string $id;
public string $className;
+ /** @readonly */
+ public TokenList $classList;
/** @implementation-alias DOMNode::hasAttributes */
public function hasAttributes(): bool {}
@@ -1661,6 +1663,31 @@ public function saveXml(?Node $node = null, int $options = 0): string|false {}
public function saveXmlFile(string $filename, int $options = 0): int|false {}
}
+ /**
+ * @not-serializable
+ * @strict-properties
+ */
+ final class TokenList implements IteratorAggregate, Countable
+ {
+ /** @implementation-alias Dom\Node::__construct */
+ private function __construct() {}
+
+ /** @readonly */
+ public int $length;
+ public function item(int $index): ?string {}
+ public function contains(string $token): bool {}
+ public function add(string ...$tokens): void {}
+ public function remove(string ...$tokens): void {}
+ public function toggle(string $token, ?bool $force = null): bool {}
+ public function replace(string $token, string $newToken): bool {}
+ public function supports(string $token): bool {}
+ public string $value;
+
+ public function count(): int {}
+
+ public function getIterator(): \Iterator {}
+ }
+
#ifdef LIBXML_XPATH_ENABLED
/** @not-serializable */
final class XPath
diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h
index f3f28bcf272a..46421eb22c2e 100644
--- a/ext/dom/php_dom_arginfo.h
+++ b/ext/dom/php_dom_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 9065d5c713a6fb879f8116821eaabc3a01a4db20 */
+ * Stub hash: 1faa01d0564052dda0dc41f36033a076b8b4c31c */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@@ -1079,6 +1079,38 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Dom_XMLDocument_saveXmlFile arginfo_class_Dom_HTMLDocument_saveXmlFile
+#define arginfo_class_Dom_TokenList___construct arginfo_class_DOMDocumentFragment___construct
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_TokenList_item, 0, 1, IS_STRING, 1)
+ ZEND_ARG_TYPE_INFO(0, index, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_TokenList_contains, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, token, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_TokenList_add, 0, 0, IS_VOID, 0)
+ ZEND_ARG_VARIADIC_TYPE_INFO(0, tokens, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Dom_TokenList_remove arginfo_class_Dom_TokenList_add
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_TokenList_toggle, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, token, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, force, _IS_BOOL, 1, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_TokenList_replace, 0, 2, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, token, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, newToken, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Dom_TokenList_supports arginfo_class_Dom_TokenList_contains
+
+#define arginfo_class_Dom_TokenList_count arginfo_class_Dom_Node_getLineNo
+
+#define arginfo_class_Dom_TokenList_getIterator arginfo_class_DOMNodeList_getIterator
+
#if defined(LIBXML_XPATH_ENABLED)
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Dom_XPath___construct, 0, 0, 1)
ZEND_ARG_OBJ_INFO(0, document, Dom\\Document, 0)
@@ -1323,6 +1355,15 @@ ZEND_METHOD(Dom_XMLDocument, createEmpty);
ZEND_METHOD(Dom_XMLDocument, createFromFile);
ZEND_METHOD(Dom_XMLDocument, createFromString);
ZEND_METHOD(Dom_XMLDocument, xinclude);
+ZEND_METHOD(Dom_TokenList, item);
+ZEND_METHOD(Dom_TokenList, contains);
+ZEND_METHOD(Dom_TokenList, add);
+ZEND_METHOD(Dom_TokenList, remove);
+ZEND_METHOD(Dom_TokenList, toggle);
+ZEND_METHOD(Dom_TokenList, replace);
+ZEND_METHOD(Dom_TokenList, supports);
+ZEND_METHOD(Dom_TokenList, count);
+ZEND_METHOD(Dom_TokenList, getIterator);
#if defined(LIBXML_XPATH_ENABLED)
ZEND_METHOD(Dom_XPath, __construct);
#endif
@@ -1839,6 +1880,20 @@ static const zend_function_entry class_Dom_XMLDocument_methods[] = {
ZEND_FE_END
};
+static const zend_function_entry class_Dom_TokenList_methods[] = {
+ ZEND_RAW_FENTRY("__construct", zim_Dom_Node___construct, arginfo_class_Dom_TokenList___construct, ZEND_ACC_PRIVATE, NULL, NULL)
+ ZEND_ME(Dom_TokenList, item, arginfo_class_Dom_TokenList_item, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, contains, arginfo_class_Dom_TokenList_contains, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, add, arginfo_class_Dom_TokenList_add, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, remove, arginfo_class_Dom_TokenList_remove, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, toggle, arginfo_class_Dom_TokenList_toggle, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, replace, arginfo_class_Dom_TokenList_replace, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, supports, arginfo_class_Dom_TokenList_supports, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, count, arginfo_class_Dom_TokenList_count, ZEND_ACC_PUBLIC)
+ ZEND_ME(Dom_TokenList, getIterator, arginfo_class_Dom_TokenList_getIterator, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
#if defined(LIBXML_XPATH_ENABLED)
static const zend_function_entry class_Dom_XPath_methods[] = {
ZEND_ME(Dom_XPath, __construct, arginfo_class_Dom_XPath___construct, ZEND_ACC_PUBLIC)
@@ -3080,6 +3135,13 @@ static zend_class_entry *register_class_Dom_Element(zend_class_entry *class_entr
zend_declare_typed_property(class_entry, property_className_name, &property_className_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
zend_string_release(property_className_name);
+ zval property_classList_default_value;
+ ZVAL_UNDEF(&property_classList_default_value);
+ zend_string *property_classList_name = zend_string_init("classList", sizeof("classList") - 1, 1);
+ zend_string *property_classList_class_Dom_TokenList = zend_string_init("Dom\\TokenList", sizeof("Dom\\TokenList")-1, 1);
+ zend_declare_typed_property(class_entry, property_classList_name, &property_classList_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_classList_class_Dom_TokenList, 0, 0));
+ zend_string_release(property_classList_name);
+
zval property_attributes_default_value;
ZVAL_UNDEF(&property_attributes_default_value);
zend_string *property_attributes_name = zend_string_init("attributes", sizeof("attributes") - 1, 1);
@@ -3571,6 +3633,30 @@ static zend_class_entry *register_class_Dom_XMLDocument(zend_class_entry *class_
return class_entry;
}
+static zend_class_entry *register_class_Dom_TokenList(zend_class_entry *class_entry_Dom_IteratorAggregate, zend_class_entry *class_entry_Dom_Countable)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "Dom", "TokenList", class_Dom_TokenList_methods);
+ class_entry = zend_register_internal_class_ex(&ce, NULL);
+ class_entry->ce_flags |= ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE;
+ zend_class_implements(class_entry, 2, class_entry_Dom_IteratorAggregate, class_entry_Dom_Countable);
+
+ zval property_length_default_value;
+ ZVAL_UNDEF(&property_length_default_value);
+ zend_string *property_length_name = zend_string_init("length", sizeof("length") - 1, 1);
+ zend_declare_typed_property(class_entry, property_length_name, &property_length_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release(property_length_name);
+
+ zval property_value_default_value;
+ ZVAL_UNDEF(&property_value_default_value);
+ zend_string *property_value_name = zend_string_init("value", sizeof("value") - 1, 1);
+ zend_declare_typed_property(class_entry, property_value_name, &property_value_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
+ zend_string_release(property_value_name);
+
+ return class_entry;
+}
+
#if defined(LIBXML_XPATH_ENABLED)
static zend_class_entry *register_class_Dom_XPath(void)
{
diff --git a/ext/dom/tests/modern/token_list/add.phpt b/ext/dom/tests/modern/token_list/add.phpt
new file mode 100644
index 000000000000..c2817682a242
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/add.phpt
@@ -0,0 +1,32 @@
+--TEST--
+TokenList: add
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+$list->add();
+$list->add('a', 'b');
+$list->add('c');
+
+$str = 'd';
+$ref =& $str;
+
+$list->add($ref);
+
+echo $dom->saveXML(), "\n";
+
+$list->value = '';
+$list->add('e');
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+
+
+
+
diff --git a/ext/dom/tests/modern/token_list/add_errors.phpt b/ext/dom/tests/modern/token_list/add_errors.phpt
new file mode 100644
index 000000000000..94db0d293114
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/add_errors.phpt
@@ -0,0 +1,41 @@
+--TEST--
+TokenList: add errors
+--EXTENSIONS--
+dom
+--FILE--
+");
+$list = $dom->documentElement->classList;
+
+try {
+ $list->add("");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->add(" ");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->add("\0");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->add(0);
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+The empty string is not a valid token
+The token must not contain any ASCII whitespace
+Dom\TokenList::add(): Argument #1 must not contain any null bytes
+Dom\TokenList::add(): Argument #1 must be of type string, int given
+
+
diff --git a/ext/dom/tests/modern/token_list/attlist.phpt b/ext/dom/tests/modern/token_list/attlist.phpt
new file mode 100644
index 000000000000..c28f0a7f6258
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/attlist.phpt
@@ -0,0 +1,47 @@
+--TEST--
+TokenList: ATTLIST interaction
+--EXTENSIONS--
+dom
+--FILE--
+
+
+
+]>
+
+XML, LIBXML_DTDATTR);
+$element = $dom->documentElement->firstChild;
+$list = $element->classList;
+
+echo 'class: ', $element->getAttribute('class'), "\n";
+var_dump($list);
+
+$list->remove('first');
+
+var_dump($list);
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+class: first second
+object(Dom\TokenList)#2 (2) {
+ ["length"]=>
+ int(2)
+ ["value"]=>
+ string(12) "first second"
+}
+object(Dom\TokenList)#2 (2) {
+ ["length"]=>
+ int(1)
+ ["value"]=>
+ string(6) "second"
+}
+
+
+
+]>
+
diff --git a/ext/dom/tests/modern/token_list/change_attribute.phpt b/ext/dom/tests/modern/token_list/change_attribute.phpt
new file mode 100644
index 000000000000..cae840063327
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/change_attribute.phpt
@@ -0,0 +1,41 @@
+--TEST--
+TokenList: change attribute
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+var_dump($list);
+
+$element->attributes[0]->value = 'd';
+
+var_dump($list);
+
+$list->value = 'e f g';
+
+var_dump($list);
+
+?>
+--EXPECT--
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(3)
+ ["value"]=>
+ string(5) "a b c"
+}
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(1)
+ ["value"]=>
+ string(1) "d"
+}
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(3)
+ ["value"]=>
+ string(5) "e f g"
+}
diff --git a/ext/dom/tests/modern/token_list/clone.phpt b/ext/dom/tests/modern/token_list/clone.phpt
new file mode 100644
index 000000000000..039551f2d43d
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/clone.phpt
@@ -0,0 +1,17 @@
+--TEST--
+TokenList: clone
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+clone $element->classList;
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Error: Trying to clone an uncloneable object of class Dom\TokenList in %s:%d
+Stack trace:
+#0 {main}
+ thrown in %s on line %d
diff --git a/ext/dom/tests/modern/token_list/contains.phpt b/ext/dom/tests/modern/token_list/contains.phpt
new file mode 100644
index 000000000000..3786769ec39d
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/contains.phpt
@@ -0,0 +1,68 @@
+--TEST--
+TokenList: contains
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+echo "---\n";
+
+var_dump($list->contains(''));
+var_dump($list->contains('A'));
+var_dump($list->contains('B'));
+var_dump($list->contains('C'));
+
+echo "---\n";
+
+var_dump($list->contains(' A'));
+var_dump($list->contains('B '));
+var_dump($list->contains(' C '));
+
+echo "---\n";
+
+var_dump($list->contains('a'));
+var_dump($list->contains('b'));
+var_dump($list->contains('c'));
+
+$element->setAttribute('class', 'D');
+
+echo "---\n";
+
+var_dump($list->contains('A'));
+var_dump($list->contains('B'));
+var_dump($list->contains('C'));
+var_dump($list->contains('D'));
+
+echo "---\n";
+
+$list->value = 'E';
+var_dump($list->contains('D'));
+var_dump($list->contains('E'));
+
+?>
+--EXPECT--
+---
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+---
+bool(false)
+bool(false)
+bool(false)
+---
+bool(false)
+bool(false)
+bool(false)
+---
+bool(false)
+bool(false)
+bool(false)
+bool(true)
+---
+bool(false)
+bool(true)
diff --git a/ext/dom/tests/modern/token_list/contains_empty.phpt b/ext/dom/tests/modern/token_list/contains_empty.phpt
new file mode 100644
index 000000000000..443be1e13d08
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/contains_empty.phpt
@@ -0,0 +1,16 @@
+--TEST--
+TokenList: contains empty
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+var_dump($list->contains('x'));
+
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/dom/tests/modern/token_list/contains_error.phpt b/ext/dom/tests/modern/token_list/contains_error.phpt
new file mode 100644
index 000000000000..9f402e685f4d
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/contains_error.phpt
@@ -0,0 +1,20 @@
+--TEST--
+TokenList: contains errors
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+try {
+ $list->contains("\0");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Dom\TokenList::contains(): Argument #1 ($token) must not contain any null bytes
diff --git a/ext/dom/tests/modern/token_list/count.phpt b/ext/dom/tests/modern/token_list/count.phpt
new file mode 100644
index 000000000000..faee8736dcf9
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/count.phpt
@@ -0,0 +1,16 @@
+--TEST--
+TokenList: count
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+var_dump($element->classList->count(), count($element->classList), $element->classList->length);
+
+?>
+--EXPECT--
+int(3)
+int(3)
+int(3)
diff --git a/ext/dom/tests/modern/token_list/debug.phpt b/ext/dom/tests/modern/token_list/debug.phpt
new file mode 100644
index 000000000000..2c56809ef98b
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/debug.phpt
@@ -0,0 +1,19 @@
+--TEST--
+TokenList: debug
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+var_dump($element->classList);
+
+?>
+--EXPECT--
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(3)
+ ["value"]=>
+ string(5) "a b c"
+}
diff --git a/ext/dom/tests/modern/token_list/dimensions.phpt b/ext/dom/tests/modern/token_list/dimensions.phpt
new file mode 100644
index 000000000000..e1507d61b08a
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/dimensions.phpt
@@ -0,0 +1,93 @@
+--TEST--
+TokenList: dimensions
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+foreach (range(-1, 4) as $i) {
+ echo "--- $i ---\n";
+ var_dump($list[$i], isset($list[$i]), empty($list[$i]));
+}
+
+echo "--- \"0\" ---\n";
+var_dump($list["0"], isset($list["0"]), empty($list["0"]));
+
+echo "--- \"foo\" ---\n";
+try {
+ var_dump($list["foo"], isset($list["foo"]), empty($list["foo"]));
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- 1.1 ---\n";
+var_dump($list[1.1], isset($list[1.1]), empty($list[1.1]));
+
+echo "--- true ---\n";
+var_dump($list[true], isset($list[true]), empty($list[true]));
+
+echo "--- false ---\n";
+var_dump($list[false], isset($list[false]), empty($list[false]));
+
+echo "--- ref ---\n";
+$tmp = 2;
+$ref =& $tmp;
+var_dump($list[$ref], isset($list[$ref]), empty($list[$ref]));
+
+?>
+--EXPECTF--
+--- -1 ---
+NULL
+bool(false)
+bool(true)
+--- 0 ---
+string(1) "A"
+bool(true)
+bool(false)
+--- 1 ---
+string(1) "B"
+bool(true)
+bool(false)
+--- 2 ---
+string(1) "C"
+bool(true)
+bool(false)
+--- 3 ---
+string(1) "0"
+bool(true)
+bool(true)
+--- 4 ---
+NULL
+bool(false)
+bool(true)
+--- "0" ---
+string(1) "A"
+bool(true)
+bool(false)
+--- "foo" ---
+Cannot access offset of type string on Dom\TokenList
+--- 1.1 ---
+
+Deprecated: Implicit conversion from float 1.1 to int loses precision in %s on line %d
+
+Deprecated: Implicit conversion from float 1.1 to int loses precision in %s on line %d
+
+Deprecated: Implicit conversion from float 1.1 to int loses precision in %s on line %d
+string(1) "B"
+bool(true)
+bool(false)
+--- true ---
+string(1) "B"
+bool(true)
+bool(false)
+--- false ---
+string(1) "A"
+bool(true)
+bool(false)
+--- ref ---
+string(1) "C"
+bool(true)
+bool(false)
diff --git a/ext/dom/tests/modern/token_list/dimensions_error.phpt b/ext/dom/tests/modern/token_list/dimensions_error.phpt
new file mode 100644
index 000000000000..c29fc058ef94
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/dimensions_error.phpt
@@ -0,0 +1,57 @@
+--TEST--
+TokenList: dimensions error
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+$testOffsets = [
+ new stdClass,
+ [],
+ fopen("php://output", "w"),
+];
+
+foreach ($testOffsets as $offset) {
+ try {
+ $list[$offset];
+ } catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+ }
+
+ try {
+ isset($list[$offset]);
+ } catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+ }
+
+ try {
+ empty($list[$offset]);
+ } catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+ }
+}
+
+try {
+ $list[][0] = 1;
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+Cannot access offset of type stdClass on Dom\TokenList
+Cannot access offset of type stdClass in isset or empty
+Cannot access offset of type stdClass in isset or empty
+Cannot access offset of type array on Dom\TokenList
+Cannot access offset of type array in isset or empty
+Cannot access offset of type array in isset or empty
+
+Warning: Resource ID#%d used as offset, casting to integer (%d) in %s on line %d
+
+Warning: Resource ID#%d used as offset, casting to integer (%d) in %s on line %d
+
+Warning: Resource ID#%d used as offset, casting to integer (%d) in %s on line %d
+Cannot append to Dom\TokenList
diff --git a/ext/dom/tests/modern/token_list/entities.phpt b/ext/dom/tests/modern/token_list/entities.phpt
new file mode 100644
index 000000000000..5ac1a52cb426
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/entities.phpt
@@ -0,0 +1,43 @@
+--TEST--
+TokenList: entities interaction
+--EXTENSIONS--
+dom
+--FILE--
+
+
+]>
+
+XML);
+$element = $dom->documentElement;
+$list = $element->classList;
+
+var_dump($list);
+
+var_dump($list->contains("xfoox"));
+var_dump($list->contains("xx"));
+var_dump($list->contains("foo"));
+
+$list->add("test");
+
+echo $dom->saveXML();
+
+?>
+--EXPECT--
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(1)
+ ["value"]=>
+ string(5) "xfoox"
+}
+bool(true)
+bool(false)
+bool(false)
+
+
+]>
+
diff --git a/ext/dom/tests/modern/token_list/equality.phpt b/ext/dom/tests/modern/token_list/equality.phpt
new file mode 100644
index 000000000000..9ed798ca4c6a
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/equality.phpt
@@ -0,0 +1,23 @@
+--TEST--
+TokenList: Test equality
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+
+var_dump($element->classList === $element->classList);
+$list = $element->classList;
+var_dump($list === $list);
+var_dump($list === $element->classList);
+
+var_dump($list === $element->firstChild->classList);
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(false)
diff --git a/ext/dom/tests/modern/token_list/foreach_by_ref.phpt b/ext/dom/tests/modern/token_list/foreach_by_ref.phpt
new file mode 100644
index 000000000000..b13b4c51c7c0
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/foreach_by_ref.phpt
@@ -0,0 +1,19 @@
+--TEST--
+TokenList: foreach by ref
+--EXTENSIONS--
+dom
+--FILE--
+');
+
+try {
+ foreach ($dom->documentElement->classList as &$class) {
+ }
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+An iterator cannot be used with foreach by reference
diff --git a/ext/dom/tests/modern/token_list/getIterator.phpt b/ext/dom/tests/modern/token_list/getIterator.phpt
new file mode 100644
index 000000000000..d90ad54ae41a
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/getIterator.phpt
@@ -0,0 +1,39 @@
+--TEST--
+TokenList: getIterator
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+$it = $list->getIterator();
+var_dump($it);
+
+var_dump($it->key(), $it->current());
+$it->next();
+var_dump($it->key(), $it->current());
+$it->next();
+var_dump($it->key(), $it->current());
+$it->next();
+var_dump($it->key(), $it->current());
+
+$it->rewind();
+var_dump($it->key(), $it->current());
+
+?>
+--EXPECT--
+object(InternalIterator)#5 (0) {
+}
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+NULL
+int(0)
+string(1) "A"
diff --git a/ext/dom/tests/modern/token_list/item.phpt b/ext/dom/tests/modern/token_list/item.phpt
new file mode 100644
index 000000000000..091be4ef0f46
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/item.phpt
@@ -0,0 +1,30 @@
+--TEST--
+TokenList: item
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+foreach (range(-1, 3) as $i) {
+ var_dump($list->item($i));
+}
+
+echo "---\n";
+
+$list->value = 'D';
+var_dump($list->item(0));
+var_dump($list->item(1));
+
+?>
+--EXPECT--
+NULL
+string(1) "A"
+string(1) "B"
+string(1) "C"
+NULL
+---
+string(1) "D"
+NULL
diff --git a/ext/dom/tests/modern/token_list/iteration_01.phpt b/ext/dom/tests/modern/token_list/iteration_01.phpt
new file mode 100644
index 000000000000..cd4621483cb5
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/iteration_01.phpt
@@ -0,0 +1,27 @@
+--TEST--
+TokenList: iteration 01
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+foreach ($list as $i => $item) {
+ var_dump($i, $item);
+ $list->remove('A', 'D');
+}
+
+?>
+--EXPECT--
+int(0)
+string(1) "A"
+int(0)
+string(1) "B"
+int(1)
+string(1) "C"
+int(2)
+string(1) "E"
+int(3)
+string(1) "F"
diff --git a/ext/dom/tests/modern/token_list/iteration_02.phpt b/ext/dom/tests/modern/token_list/iteration_02.phpt
new file mode 100644
index 000000000000..39f921a8a5a0
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/iteration_02.phpt
@@ -0,0 +1,117 @@
+--TEST--
+TokenList: iteration 02
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+foreach ($list as $i => $item) {
+ var_dump($i, $item);
+ echo "==========\n";
+ foreach ($list as $i2 => $item2) {
+ var_dump($i2, $item2);
+ }
+ echo "==========\n";
+}
+
+?>
+--EXPECT--
+int(0)
+string(1) "A"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
+int(1)
+string(1) "B"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
+int(2)
+string(1) "C"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
+int(3)
+string(1) "D"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
+int(4)
+string(1) "E"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
+int(5)
+string(1) "F"
+==========
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "C"
+int(3)
+string(1) "D"
+int(4)
+string(1) "E"
+int(5)
+string(1) "F"
+==========
diff --git a/ext/dom/tests/modern/token_list/iterator_modification_invalidation.phpt b/ext/dom/tests/modern/token_list/iterator_modification_invalidation.phpt
new file mode 100644
index 000000000000..f013d7615057
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/iterator_modification_invalidation.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test DOMTokenList iterator invalidation after modification
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+
+echo "---\n";
+
+$counter = 0;
+foreach ($list as $key => $token) {
+ var_dump($key, $token);
+ if (++$counter === 2) {
+ $list->value = 'E F G';
+ }
+}
+
+echo "---\n";
+
+$iterator = $list->getIterator();
+$iterator->next();
+$list->value = 'X Y Z';
+var_dump($iterator->key());
+var_dump($iterator->current());
+$iterator->rewind();
+var_dump($iterator->key());
+var_dump($iterator->current());
+$list->value = '';
+var_dump($iterator->key());
+var_dump($iterator->current());
+var_dump($iterator->valid());
+
+?>
+--EXPECT--
+---
+int(0)
+string(1) "A"
+int(1)
+string(1) "B"
+int(2)
+string(1) "G"
+---
+int(1)
+string(1) "Y"
+int(0)
+string(1) "X"
+int(0)
+NULL
+bool(false)
diff --git a/ext/dom/tests/modern/token_list/remove.phpt b/ext/dom/tests/modern/token_list/remove.phpt
new file mode 100644
index 000000000000..d6d8f05c4fd2
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/remove.phpt
@@ -0,0 +1,38 @@
+--TEST--
+TokenList: remove
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+$list->remove();
+$list->remove('test1');
+
+echo $dom->saveXML(), "\n";
+
+$list->remove('nope');
+
+echo $dom->saveXML(), "\n";
+
+$list->remove('test2');
+
+echo $dom->saveXML(), "\n";
+
+$list->value = 'test3 test4';
+$list->remove('test4');
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+
+
+
+
+
+
+
+
diff --git a/ext/dom/tests/modern/token_list/remove_errors.phpt b/ext/dom/tests/modern/token_list/remove_errors.phpt
new file mode 100644
index 000000000000..fe0e58c334c9
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/remove_errors.phpt
@@ -0,0 +1,41 @@
+--TEST--
+TokenList: remove errors
+--EXTENSIONS--
+dom
+--FILE--
+");
+$list = $dom->documentElement->classList;
+
+try {
+ $list->remove("");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->remove(" ");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->remove("\0");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $list->remove(0);
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+The empty string is not a valid token
+The token must not contain any ASCII whitespace
+Dom\TokenList::remove(): Argument #1 must not contain any null bytes
+Dom\TokenList::remove(): Argument #1 must be of type string, int given
+
+
diff --git a/ext/dom/tests/modern/token_list/removed_element.phpt b/ext/dom/tests/modern/token_list/removed_element.phpt
new file mode 100644
index 000000000000..548aa30e6fff
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/removed_element.phpt
@@ -0,0 +1,33 @@
+--TEST--
+TokenList: operate on removed element
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+$element->remove();
+
+var_dump($list);
+
+$list->remove('B');
+
+var_dump($list);
+
+?>
+--EXPECT--
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(3)
+ ["value"]=>
+ string(5) "A B C"
+}
+object(Dom\TokenList)#3 (2) {
+ ["length"]=>
+ int(2)
+ ["value"]=>
+ string(3) "A C"
+}
diff --git a/ext/dom/tests/modern/token_list/replace.phpt b/ext/dom/tests/modern/token_list/replace.phpt
new file mode 100644
index 000000000000..7d8f81ab9784
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/replace.phpt
@@ -0,0 +1,55 @@
+--TEST--
+TokenList: replace
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+var_dump($list->replace('nonexistent', 'X'));
+
+echo $dom->saveXML(), "\n";
+
+var_dump($list->replace('B', 'X'));
+
+echo $dom->saveXML(), "\n";
+
+var_dump($list->replace('C', 'X'));
+
+echo $dom->saveXML(), "\n";
+
+var_dump($list->replace('A', 'B'));
+
+echo $dom->saveXML(), "\n";
+
+var_dump($list->replace('X', 'B'));
+
+echo $dom->saveXML(), "\n";
+
+$list->value = 'A';
+$list->replace('A', 'AA');
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+bool(false)
+
+
+bool(true)
+
+
+bool(true)
+
+
+bool(true)
+
+
+bool(true)
+
+
+
+
diff --git a/ext/dom/tests/modern/token_list/replace_error.phpt b/ext/dom/tests/modern/token_list/replace_error.phpt
new file mode 100644
index 000000000000..14b02373d57d
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/replace_error.phpt
@@ -0,0 +1,34 @@
+--TEST--
+TokenList: replace errors
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+try {
+ $list->replace("\0", "X");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $list->replace("X", "\0");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $list->replace("a b", "X");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Dom\TokenList::replace(): Argument #1 ($token) must not contain any null bytes
+Dom\TokenList::replace(): Argument #2 ($newToken) must not contain any null bytes
+The token must not contain any ASCII whitespace
diff --git a/ext/dom/tests/modern/token_list/supports.phpt b/ext/dom/tests/modern/token_list/supports.phpt
new file mode 100644
index 000000000000..6fc1d7f22549
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/supports.phpt
@@ -0,0 +1,18 @@
+--TEST--
+TokenList: supports
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+try {
+ $element->classList->supports('a');
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Attribute "class" does not define any supported tokens
diff --git a/ext/dom/tests/modern/token_list/toggle.phpt b/ext/dom/tests/modern/token_list/toggle.phpt
new file mode 100644
index 000000000000..494368024b64
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/toggle.phpt
@@ -0,0 +1,83 @@
+--TEST--
+TokenList: toggle
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+echo "--- Toggle A (forced add) ---\n";
+
+var_dump($list->toggle("A", true));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle A (not forced) ---\n";
+
+var_dump($list->toggle("A"));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle A (forced remove) ---\n";
+
+var_dump($list->toggle("A", false));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle B (forced remove) ---\n";
+
+var_dump($list->toggle("B", false));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle D ---\n";
+
+var_dump($list->toggle("D"));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle C ---\n";
+
+var_dump($list->toggle("C"));
+
+echo $dom->saveXML(), "\n";
+
+echo "--- Toggle E ---\n";
+
+$list->value = 'E';
+$list->toggle('E');
+
+echo $dom->saveXML(), "\n";
+
+?>
+--EXPECT--
+--- Toggle A (forced add) ---
+bool(true)
+
+
+--- Toggle A (not forced) ---
+bool(false)
+
+
+--- Toggle A (forced remove) ---
+bool(false)
+
+
+--- Toggle B (forced remove) ---
+bool(false)
+
+
+--- Toggle D ---
+bool(true)
+
+
+--- Toggle C ---
+bool(false)
+
+
+--- Toggle E ---
+
+
diff --git a/ext/dom/tests/modern/token_list/toggle_error.phpt b/ext/dom/tests/modern/token_list/toggle_error.phpt
new file mode 100644
index 000000000000..7e3cfd462023
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/toggle_error.phpt
@@ -0,0 +1,27 @@
+--TEST--
+TokenList: toggle errors
+--EXTENSIONS--
+dom
+--FILE--
+');
+$element = $dom->documentElement;
+$list = $element->classList;
+
+try {
+ $list->toggle("\0");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $list->toggle("a b");
+} catch (DOMException $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Dom\TokenList::toggle(): Argument #1 ($token) must not contain any null bytes
+The token must not contain any ASCII whitespace
diff --git a/ext/dom/tests/modern/token_list/value_edge_cases.phpt b/ext/dom/tests/modern/token_list/value_edge_cases.phpt
new file mode 100644
index 000000000000..af32d84180ba
--- /dev/null
+++ b/ext/dom/tests/modern/token_list/value_edge_cases.phpt
@@ -0,0 +1,25 @@
+--TEST--
+TokenList: value edge cases
+--EXTENSIONS--
+dom
+--FILE--
+');
+$list = $dom->documentElement->classList;
+
+var_dump($list->value);
+
+try {
+ $list->value = "\0";
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump($list->value);
+
+?>
+--EXPECT--
+string(0) ""
+Value must not contain any null bytes
+string(0) ""
diff --git a/ext/dom/token_list.c b/ext/dom/token_list.c
new file mode 100644
index 000000000000..15eaeb402017
--- /dev/null
+++ b/ext/dom/token_list.c
@@ -0,0 +1,732 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | https://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Authors: Niels Dossche |
+ +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+#include
+#endif
+
+#include "php.h"
+#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
+#include "php_dom.h"
+#include "token_list.h"
+#include "infra.h"
+#include "zend_interfaces.h"
+
+#define TOKEN_LIST_GET_INTERNAL() php_dom_token_list_from_obj(Z_OBJ_P(ZEND_THIS))
+#define TOKEN_LIST_GET_SET(intern) (&(intern)->token_set)
+#define Z_TOKEN_LIST_P(zv) php_dom_token_list_from_obj(Z_OBJ_P(zv))
+
+typedef struct dom_token_list_it {
+ zend_object_iterator it;
+ /* Store the hash position here to allow multiple (e.g. nested) iterations of the same token list. */
+ HashPosition pos;
+ php_libxml_cache_tag cache_tag;
+} dom_token_list_it;
+
+static zend_always_inline bool dom_contains_ascii_whitespace(const char *data)
+{
+ return strpbrk(data, ascii_whitespace) != NULL;
+}
+
+static zend_always_inline void dom_add_token(HashTable *ht, zend_string *token)
+{
+ /* Key outlives the value's lifetime because as long as the entry is in the table it is kept alive. */
+ zval zv;
+ ZVAL_STR(&zv, token);
+ zend_hash_add(ht, token, &zv);
+}
+
+/* https://dom.spec.whatwg.org/#concept-ordered-set-parser
+ * and https://infra.spec.whatwg.org/#split-on-ascii-whitespace */
+static void dom_ordered_set_parser(HashTable *token_set, const char *position)
+{
+ /* Adapted steps from "split on ASCII whitespace" such that that loop directly appends to the token set. */
+
+ /* 1. Let position be a position variable for input, initially pointing at the start of input.
+ * => That's the position pointer. */
+ /* 2. Let tokens be a list of strings, initially empty.
+ * => That's the token set. */
+
+ /* 3. Skip ASCII whitespace within input given position. */
+ position += strspn(position, ascii_whitespace);
+
+ /* 4. While position is not past the end of input: */
+ while (*position != '\0') {
+ /* 4.1. Let token be the result of collecting a sequence of code points that are not ASCII whitespace from input */
+ const char *start = position;
+ position += strcspn(position, ascii_whitespace);
+ size_t length = position - start;
+
+ /* 4.2. Append token to tokens. */
+ zend_string *token = zend_string_init(start, length, false);
+ dom_add_token(token_set, token);
+ zend_string_release_ex(token, false);
+
+ /* 4.3. Skip ASCII whitespace within input given position. */
+ position += strspn(position, ascii_whitespace);
+ }
+
+ /* 5. Return tokens.
+ * => That's the token set. */
+}
+
+/* https://dom.spec.whatwg.org/#concept-ordered-set-serializer */
+static char *dom_ordered_set_serializer(HashTable *token_set)
+{
+ size_t length = 0;
+ zend_string *token;
+ ZEND_HASH_MAP_FOREACH_STR_KEY(token_set, token) {
+ size_t needed_size = ZSTR_LEN(token) + 1; /* +1 for the space (or \0 at the end) */
+ if (UNEXPECTED(ZSTR_MAX_LEN - length < needed_size)) {
+ /* Shouldn't really be able to happen in practice. */
+ zend_throw_error(NULL, "Token set too large");
+ return NULL;
+ }
+ length += needed_size;
+ } ZEND_HASH_FOREACH_END();
+
+ if (length == 0) {
+ char *ret = emalloc(1);
+ *ret = '\0';
+ return ret;
+ }
+
+ char *ret = emalloc(length);
+ char *ptr = ret;
+ ZEND_HASH_MAP_FOREACH_STR_KEY(token_set, token) {
+ memcpy(ptr, ZSTR_VAL(token), ZSTR_LEN(token));
+ ptr += ZSTR_LEN(token);
+ *ptr++ = ' ';
+ } ZEND_HASH_FOREACH_END();
+ ptr[-1] = '\0'; /* replace last space with \0 */
+ return ret;
+}
+
+static zend_always_inline xmlNode *dom_token_list_get_element(dom_token_list_object *intern)
+{
+ php_libxml_node_ptr *element_ptr = intern->dom.ptr;
+ return element_ptr->node;
+}
+
+static zend_always_inline const xmlAttr *dom_token_list_get_attr(dom_token_list_object *intern)
+{
+ const xmlNode *element_node = dom_token_list_get_element(intern);
+ return xmlHasNsProp(element_node, BAD_CAST "class", NULL);
+}
+
+/* https://dom.spec.whatwg.org/#concept-dtl-update */
+static void dom_token_list_update(dom_token_list_object *intern)
+{
+ const xmlAttr *attr = dom_token_list_get_attr(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+
+ php_libxml_invalidate_cache_tag(&intern->cache_tag);
+
+ /* 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) {
+ return;
+ }
+
+ /* 2. Set an attribute value for the associated element using associated attribute’s local name and the result of
+ * running the ordered set serializer for token set. */
+ char *value = dom_ordered_set_serializer(token_set);
+ xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST value);
+ efree(intern->cached_string);
+ intern->cached_string = value;
+}
+
+static xmlChar *dom_token_list_get_class_value(const xmlAttr *attr, bool *should_free)
+{
+ if (attr != NULL && attr->children != NULL) {
+ return php_libxml_attr_value(attr, should_free);
+ }
+ *should_free = false;
+ return NULL;
+}
+
+static void dom_token_list_update_set(dom_token_list_object *intern, HashTable *token_set)
+{
+ /* https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A1 */
+ bool should_free;
+ const xmlAttr *attr = dom_token_list_get_attr(intern);
+ /* 1. If the data is null, the token set remains empty. */
+ xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
+ if (value != NULL) {
+ /* 2. Otherwise, parse the token set. */
+ dom_ordered_set_parser(token_set, (const char *) value);
+ intern->cached_string = estrdup((const char *) value);
+ } else {
+ intern->cached_string = NULL;
+ }
+
+ if (should_free) {
+ xmlFree(value);
+ }
+}
+
+static void dom_token_list_ensure_set_up_to_date(dom_token_list_object *intern)
+{
+ bool should_free;
+ const xmlAttr *attr = dom_token_list_get_attr(intern);
+ xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
+
+ /* xmlStrEqual will automatically handle equality rules of NULL vs "" (etc.) correctly. */
+ if (!xmlStrEqual(value, (const xmlChar *) intern->cached_string)) {
+ php_libxml_invalidate_cache_tag(&intern->cache_tag);
+ efree(intern->cached_string);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ zend_hash_destroy(token_set);
+ zend_hash_init(token_set, 0, NULL, NULL, false);
+ dom_token_list_update_set(intern, token_set);
+ }
+
+ if (should_free) {
+ xmlFree(value);
+ }
+}
+
+void dom_token_list_ctor(dom_token_list_object *intern, dom_object *element_obj)
+{
+ php_libxml_node_ptr *ptr = element_obj->ptr;
+ ptr->refcount++;
+ intern->dom.ptr = ptr;
+ element_obj->document->refcount++;
+ intern->dom.document = element_obj->document;
+
+ intern->cache_tag.modification_nr = 0;
+
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ zend_hash_init(token_set, 0, NULL, NULL, false);
+
+ dom_token_list_update_set(intern, token_set);
+}
+
+void dom_token_list_free_obj(zend_object *object)
+{
+ dom_token_list_object *intern = php_dom_token_list_from_obj(object);
+
+ zend_object_std_dtor(object);
+
+ if (EXPECTED(intern->dom.ptr != NULL)) { /* Object initialized? */
+ xmlNodePtr node = dom_token_list_get_element(intern);
+ if (php_libxml_decrement_node_ptr_ref(intern->dom.ptr) == 0) {
+ php_libxml_node_free_resource(node);
+ }
+ php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ zend_hash_destroy(token_set);
+ efree(intern->cached_string);
+ }
+}
+
+static bool dom_token_list_item_exists(dom_token_list_object *token_list, zend_long index)
+{
+ dom_token_list_ensure_set_up_to_date(token_list);
+
+ HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
+ return index >= 0 && index < zend_hash_num_elements(token_set);
+}
+
+static void dom_token_list_item_read(dom_token_list_object *token_list, zval *retval, zend_long index)
+{
+ dom_token_list_ensure_set_up_to_date(token_list);
+
+ HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
+ if (index >= 0 && index < zend_hash_num_elements(token_set)) {
+ HashPosition position;
+ zend_hash_internal_pointer_reset_ex(token_set, &position);
+ while (index > 0) {
+ zend_hash_move_forward_ex(token_set, &position);
+ index--;
+ }
+ zend_string *str_index;
+ zend_hash_get_current_key_ex(token_set, &str_index, NULL, &position);
+ ZVAL_STR_COPY(retval, str_index);
+ } else {
+ /* Not an out of bounds ValueError, but NULL, as according to spec.
+ * This design choice allows for constructs like `item(x) ?? ...`
+ *
+ * In particular:
+ * https://dom.spec.whatwg.org/#interface-domtokenlist states DOMTokenList implements iterable.
+ * From https://webidl.spec.whatwg.org/#idl-iterable:
+ * If a single type parameter is given,
+ * then the interface has a value iterator and provides values of the specified type.
+ * This applies, and reading the definition of value iterator means we should support indexed properties.
+ * From https://webidl.spec.whatwg.org/#dfn-support-indexed-properties:
+ * An interface that defines an indexed property getter is said to support indexed properties.
+ * And indexed property getter is defined here: https://webidl.spec.whatwg.org/#dfn-indexed-property-getter
+ * Down below in their note they give an example of how an out-of-bounds access evaluates to undefined,
+ * which would map to NULL for us.
+ * This would also be consistent with how out-of-bounds array accesses in PHP result in NULL. */
+ ZVAL_NULL(retval);
+ }
+}
+
+/* Adapted from spl_offset_convert_to_long */
+static zend_long dom_token_list_offset_convert_to_long(zval *offset, bool *failed)
+{
+ *failed = false;
+
+ while (true) {
+ switch (Z_TYPE_P(offset)) {
+ case IS_STRING: {
+ zend_ulong index;
+ if (ZEND_HANDLE_NUMERIC(Z_STR_P(offset), index)) {
+ return (zend_long) index;
+ }
+ ZEND_FALLTHROUGH;
+ }
+ default:
+ *failed = true;
+ return 0;
+ case IS_DOUBLE:
+ return zend_dval_to_lval_safe(Z_DVAL_P(offset));
+ case IS_LONG:
+ return Z_LVAL_P(offset);
+ case IS_FALSE:
+ return 0;
+ case IS_TRUE:
+ return 1;
+ case IS_REFERENCE:
+ offset = Z_REFVAL_P(offset);
+ break;
+ case IS_RESOURCE:
+ zend_use_resource_as_offset(offset);
+ return Z_RES_HANDLE_P(offset);
+ }
+ }
+}
+
+zval *dom_token_list_read_dimension(zend_object *object, zval *offset, int type, zval *rv)
+{
+ if (!offset) {
+ zend_throw_error(NULL, "Cannot append to Dom\\TokenList");
+ return NULL;
+ }
+
+ bool failed;
+ zend_long index = dom_token_list_offset_convert_to_long(offset, &failed);
+ if (UNEXPECTED(failed)) {
+ zend_illegal_container_offset(object->ce->name, offset, type);
+ return NULL;
+ } else {
+ dom_token_list_item_read(php_dom_token_list_from_obj(object), rv, index);
+ return rv;
+ }
+}
+
+int dom_token_list_has_dimension(zend_object *object, zval *offset, int check_empty)
+{
+ bool failed;
+ zend_long index = dom_token_list_offset_convert_to_long(offset, &failed);
+ if (UNEXPECTED(failed)) {
+ zend_illegal_container_offset(object->ce->name, offset, BP_VAR_IS);
+ return 0;
+ } else {
+ dom_token_list_object *token_list = php_dom_token_list_from_obj(object);
+ if (check_empty) {
+ /* Need to perform an actual read to have the correct empty() semantics. */
+ zval rv;
+ dom_token_list_item_read(token_list, &rv, index);
+ int is_true = zend_is_true(&rv);
+ zval_ptr_dtor_nogc(&rv);
+ return is_true;
+ } else {
+ return dom_token_list_item_exists(token_list, index);
+ }
+ }
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-length */
+zend_result dom_token_list_length_read(dom_object *obj, zval *retval)
+{
+ dom_token_list_object *token_list = php_dom_token_list_from_dom_obj(obj);
+ dom_token_list_ensure_set_up_to_date(token_list);
+ ZVAL_LONG(retval, zend_hash_num_elements(TOKEN_LIST_GET_SET(token_list)));
+ return SUCCESS;
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-value
+ * and https://dom.spec.whatwg.org/#concept-dtl-serialize */
+zend_result dom_token_list_value_read(dom_object *obj, zval *retval)
+{
+ bool should_free;
+ dom_token_list_object *intern = php_dom_token_list_from_dom_obj(obj);
+ const xmlAttr *attr = dom_token_list_get_attr(intern);
+ xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
+ ZVAL_STRING(retval, value ? (const char *) value : "");
+ if (should_free) {
+ xmlFree(value);
+ }
+ return SUCCESS;
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-value */
+zend_result dom_token_list_value_write(dom_object *obj, zval *newval)
+{
+ dom_token_list_object *intern = php_dom_token_list_from_dom_obj(obj);
+ if (UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(newval)))) {
+ zend_value_error("Value must not contain any null bytes");
+ return FAILURE;
+ }
+ 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;
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-item */
+PHP_METHOD(Dom_TokenList, item)
+{
+ zend_long index;
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_LONG(index)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* 1. If index is equal to or greater than this’s token set’s size, then return null. */
+ /* 2. Return this’s token set[index]. */
+ dom_token_list_item_read(TOKEN_LIST_GET_INTERNAL(), return_value, index);
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-contains */
+PHP_METHOD(Dom_TokenList, contains)
+{
+ zend_string *token;
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_PATH_STR(token)
+ ZEND_PARSE_PARAMETERS_END();
+
+ dom_token_list_object *token_list = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(token_list);
+ HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
+ RETURN_BOOL(zend_hash_exists(token_set, token));
+}
+
+/* Steps taken from the add, remove, toggle, replace methods. */
+static bool dom_validate_token(const zend_string *str)
+{
+ /* 1. If token is the empty string, then throw a "SyntaxError" DOMException. */
+ if (ZSTR_LEN(str) == 0) {
+ php_dom_throw_error_with_message(SYNTAX_ERR, "The empty string is not a valid token", true);
+ return false;
+ }
+
+ /* 2. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. */
+ if (dom_contains_ascii_whitespace(ZSTR_VAL(str))) {
+ php_dom_throw_error_with_message(INVALID_CHARACTER_ERR, "The token must not contain any ASCII whitespace", true);
+ return false;
+ }
+
+ return true;
+}
+
+static bool dom_validate_tokens_varargs(const zval *args, uint32_t argc)
+{
+ for (uint32_t i = 0; i < argc; i++) {
+ if (Z_TYPE(args[i]) != IS_STRING) {
+ zend_argument_type_error(i + 1, "must be of type string, %s given", zend_zval_value_name(&args[i]));
+ return false;
+ }
+
+ if (zend_str_has_nul_byte(Z_STR(args[i]))) {
+ zend_argument_value_error(i + 1, "must not contain any null bytes");
+ return false;
+ }
+
+ if (!dom_validate_token(Z_STR(args[i]))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-add */
+PHP_METHOD(Dom_TokenList, add)
+{
+ zval *args;
+ uint32_t argc;
+ ZEND_PARSE_PARAMETERS_START(0, -1)
+ Z_PARAM_VARIADIC('*', args, argc)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* 1. For each token in tokens (...) */
+ if (!dom_validate_tokens_varargs(args, argc)) {
+ RETURN_THROWS();
+ }
+
+ /* 2. For each token in tokens, append token to this’s token set. */
+ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ for (uint32_t i = 0; i < argc; i++) {
+ dom_add_token(token_set, Z_STR(args[i]));
+ }
+
+ /* 3. Run the update steps. */
+ dom_token_list_update(intern);
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-remove */
+PHP_METHOD(Dom_TokenList, remove)
+{
+ zval *args;
+ uint32_t argc;
+ ZEND_PARSE_PARAMETERS_START(0, -1)
+ Z_PARAM_VARIADIC('*', args, argc)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* 1. For each token in tokens (...) */
+ if (!dom_validate_tokens_varargs(args, argc)) {
+ RETURN_THROWS();
+ }
+
+ /* 2. For each token in tokens, remove token from this’s token set. */
+ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ for (uint32_t i = 0; i < argc; i++) {
+ zend_hash_del(token_set, Z_STR(args[i]));
+ }
+
+ /* 3. Run the update steps. */
+ dom_token_list_update(intern);
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-toggle */
+PHP_METHOD(Dom_TokenList, toggle)
+{
+ zend_string *token;
+ bool force, force_not_given = true;
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_PATH_STR(token)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_BOOL_OR_NULL(force, force_not_given)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* Steps 1 - 2 */
+ if (!dom_validate_token(token)) {
+ RETURN_THROWS();
+ }
+
+ /* 3. If this’s token set[token] exists, then: */
+ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ zval *found_token = zend_hash_find(token_set, token);
+ if (found_token != NULL) {
+ ZEND_ASSERT(XtOffsetOf(Bucket, val) == 0 && "the cast only works if this is true");
+ Bucket *bucket = (Bucket *) found_token;
+
+ /* 3.1. If force is either not given or is false, then remove token from this’s token set,
+ * run the update steps and return false. */
+ if (force_not_given || !force) {
+ zend_hash_del_bucket(token_set, bucket);
+ dom_token_list_update(intern);
+ RETURN_FALSE;
+ }
+
+ /* 3.2. Return true. */
+ RETURN_TRUE;
+ }
+ /* 4. Otherwise, if force not given or is true, append token to this’s token set,
+ * run the update steps, and return true. */
+ else if (force_not_given || force) {
+ dom_add_token(token_set, token);
+ dom_token_list_update(intern);
+ RETURN_TRUE;
+ }
+
+ /* 5. Return false. */
+ RETURN_FALSE;
+}
+
+/* https://dom.spec.whatwg.org/#dom-domtokenlist-replace */
+PHP_METHOD(Dom_TokenList, replace)
+{
+ zend_string *token, *new_token;
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_PATH_STR(token)
+ Z_PARAM_PATH_STR(new_token)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* Steps 1 - 2 */
+ if (!dom_validate_token(token) || !dom_validate_token(new_token)) {
+ RETURN_THROWS();
+ }
+
+ /* 3. If this’s token set does not contain token, then return false. */
+ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+ zval *found_token = zend_hash_find(token_set, token);
+ if (found_token == NULL) {
+ RETURN_FALSE;
+ }
+
+ /* 4. Replace token in this’s token set with newToken. */
+ ZEND_ASSERT(XtOffsetOf(Bucket, val) == 0 && "the cast only works if this is true");
+ Bucket *bucket = (Bucket *) found_token;
+ if (zend_hash_set_bucket_key(token_set, bucket, new_token) == NULL) {
+ /* It already exists, remove token instead. */
+ zend_hash_del_bucket(token_set, bucket);
+ } else {
+ Z_STR(bucket->val) = new_token;
+ }
+
+ /* 5. Run the update steps. */
+ dom_token_list_update(intern);
+
+ /* 6. Return true. */
+ RETURN_TRUE;
+}
+
+/* https://dom.spec.whatwg.org/#concept-domtokenlist-validation */
+PHP_METHOD(Dom_TokenList, supports)
+{
+ zend_string *token;
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_PATH_STR(token)
+ ZEND_PARSE_PARAMETERS_END();
+
+ /* The spec designers have designed the TokenList API with future usages in mind.
+ * But right now, this should just always throw a TypeError because the only user is classList, which
+ * does not define a supported token set. */
+ zend_throw_error(zend_ce_type_error, "Attribute \"class\" does not define any supported tokens");
+}
+
+PHP_METHOD(Dom_TokenList, count)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
+ dom_token_list_ensure_set_up_to_date(intern);
+ RETURN_LONG(zend_hash_num_elements(TOKEN_LIST_GET_SET(intern)));
+}
+
+PHP_METHOD(Dom_TokenList, getIterator)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ zend_create_internal_iterator_zval(return_value, ZEND_THIS);
+}
+
+static void dom_token_list_it_dtor(zend_object_iterator *iter)
+{
+ zval_ptr_dtor(&iter->data);
+}
+
+static void dom_token_list_it_rewind(zend_object_iterator *iter)
+{
+ dom_token_list_it *iterator = (dom_token_list_it *) iter;
+ dom_token_list_object *object = Z_TOKEN_LIST_P(&iter->data);
+ zend_hash_internal_pointer_reset_ex(TOKEN_LIST_GET_SET(object), &iterator->pos);
+}
+
+static zend_result dom_token_list_it_valid(zend_object_iterator *iter)
+{
+ dom_token_list_it *iterator = (dom_token_list_it *) iter;
+ dom_token_list_object *object = Z_TOKEN_LIST_P(&iter->data);
+ HashTable *token_set = TOKEN_LIST_GET_SET(object);
+
+ dom_token_list_ensure_set_up_to_date(object);
+
+ iterator->pos = zend_hash_get_current_pos_ex(token_set, iterator->pos);
+
+ return iterator->pos >= token_set->nNumUsed ? FAILURE : SUCCESS;
+}
+
+static zval *dom_token_list_it_get_current_data(zend_object_iterator *iter)
+{
+ dom_token_list_it *iterator = (dom_token_list_it *) iter;
+ dom_token_list_object *object = Z_TOKEN_LIST_P(&iter->data);
+ dom_token_list_ensure_set_up_to_date(object);
+ /* Caller manages the refcount of the data. */
+ return zend_hash_get_current_data_ex(TOKEN_LIST_GET_SET(object), &iterator->pos);
+}
+
+static void dom_token_list_it_get_current_key(zend_object_iterator *iter, zval *key)
+{
+ dom_token_list_it *iterator = (dom_token_list_it *) iter;
+ dom_token_list_object *object = Z_TOKEN_LIST_P(&iter->data);
+
+ dom_token_list_ensure_set_up_to_date(object);
+
+ if (UNEXPECTED(php_libxml_is_cache_tag_stale(&object->cache_tag, &iterator->cache_tag))) {
+ iter->index = 0;
+ HashPosition pos;
+ HashTable *token_set = TOKEN_LIST_GET_SET(object);
+ zend_hash_internal_pointer_reset_ex(token_set, &pos);
+ while (pos != iterator->pos) {
+ iter->index++;
+ zend_hash_move_forward_ex(token_set, &pos);
+ }
+ }
+
+ ZVAL_LONG(key, iter->index);
+}
+
+static void dom_token_list_it_move_forward(zend_object_iterator *iter)
+{
+ dom_token_list_it *iterator = (dom_token_list_it *) iter;
+ dom_token_list_object *object = Z_TOKEN_LIST_P(&iter->data);
+ HashTable *token_set = TOKEN_LIST_GET_SET(object);
+
+ dom_token_list_ensure_set_up_to_date(object);
+
+ HashPosition current = iterator->pos;
+ HashPosition validated = zend_hash_get_current_pos_ex(token_set, iterator->pos);
+
+ /* Check if already moved due to user operations, if so don't move again but reset to the first valid position,
+ * otherwise move one forward. */
+ if (validated != current) {
+ iterator->pos = validated;
+ } else {
+ zend_hash_move_forward_ex(token_set, &iterator->pos);
+ }
+}
+
+static const zend_object_iterator_funcs dom_token_list_it_funcs = {
+ dom_token_list_it_dtor,
+ dom_token_list_it_valid,
+ dom_token_list_it_get_current_data,
+ dom_token_list_it_get_current_key,
+ dom_token_list_it_move_forward,
+ dom_token_list_it_rewind,
+ NULL, /* invalidate_current */
+ NULL, /* get_gc */
+};
+
+zend_object_iterator *dom_token_list_get_iterator(zend_class_entry *ce, zval *object, int by_ref)
+{
+ if (by_ref) {
+ zend_throw_error(NULL, "An iterator cannot be used with foreach by reference");
+ return NULL;
+ }
+
+ dom_token_list_object *intern = Z_TOKEN_LIST_P(object);
+ dom_token_list_ensure_set_up_to_date(intern);
+ HashTable *token_set = TOKEN_LIST_GET_SET(intern);
+
+ dom_token_list_it *iterator = emalloc(sizeof(*iterator));
+ zend_iterator_init(&iterator->it);
+ zend_hash_internal_pointer_reset_ex(token_set, &iterator->pos);
+ ZVAL_OBJ_COPY(&iterator->it.data, Z_OBJ_P(object));
+
+ iterator->it.funcs = &dom_token_list_it_funcs;
+ iterator->cache_tag = intern->cache_tag;
+
+ return &iterator->it;
+}
+
+#endif
diff --git a/ext/dom/token_list.h b/ext/dom/token_list.h
new file mode 100644
index 000000000000..4711852c4d87
--- /dev/null
+++ b/ext/dom/token_list.h
@@ -0,0 +1,44 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | https://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Authors: Niels Dossche |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef TOKEN_LIST_H
+#define TOKEN_LIST_H
+
+typedef struct dom_token_list_object {
+ HashTable token_set;
+ /* Used to check if the token set is up to date. */
+ char *cached_string;
+ php_libxml_cache_tag cache_tag;
+ dom_object dom;
+} dom_token_list_object;
+
+static inline dom_token_list_object *php_dom_token_list_from_obj(zend_object *obj)
+{
+ return (dom_token_list_object *)((char *) obj - XtOffsetOf(dom_token_list_object, dom.std));
+}
+
+static inline dom_token_list_object *php_dom_token_list_from_dom_obj(dom_object *obj)
+{
+ return (dom_token_list_object *)((char *) obj - XtOffsetOf(dom_token_list_object, dom));
+}
+
+void dom_token_list_ctor(dom_token_list_object *intern, dom_object *element_obj);
+void dom_token_list_free_obj(zend_object *object);
+zval *dom_token_list_read_dimension(zend_object *object, zval *offset, int type, zval *rv);
+int dom_token_list_has_dimension(zend_object *object, zval *offset, int check_empty);
+zend_object_iterator *dom_token_list_get_iterator(zend_class_entry *ce, zval *object, int by_ref);
+
+#endif
diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c
index 0276ef0c26cb..6955a1a36379 100644
--- a/ext/libxml/libxml.c
+++ b/ext/libxml/libxml.c
@@ -1313,26 +1313,32 @@ PHP_LIBXML_API int php_libxml_increment_node_ptr(php_libxml_node_object *object,
return ret_refcount;
}
-PHP_LIBXML_API int php_libxml_decrement_node_ptr(php_libxml_node_object *object)
+PHP_LIBXML_API int php_libxml_decrement_node_ptr_ref(php_libxml_node_ptr *ptr)
{
- int ret_refcount = -1;
- php_libxml_node_ptr *obj_node;
+ ZEND_ASSERT(ptr != NULL);
- if (object != NULL && object->node != NULL) {
- obj_node = (php_libxml_node_ptr *) object->node;
- ret_refcount = --obj_node->refcount;
- if (ret_refcount == 0) {
- if (obj_node->node != NULL) {
- obj_node->node->_private = NULL;
- }
- efree(obj_node);
+ int ret_refcount = --ptr->refcount;
+ if (ret_refcount == 0) {
+ if (ptr->node != NULL) {
+ ptr->node->_private = NULL;
+ }
+ if (ptr->_private) {
+ php_libxml_node_object *object = (php_libxml_node_object *) ptr->_private;
+ object->node = NULL;
}
- object->node = NULL;
+ efree(ptr);
}
-
return ret_refcount;
}
+PHP_LIBXML_API int php_libxml_decrement_node_ptr(php_libxml_node_object *object)
+{
+ if (object != NULL && object->node != NULL) {
+ return php_libxml_decrement_node_ptr_ref(object->node);
+ }
+ return -1;
+}
+
PHP_LIBXML_API int php_libxml_increment_doc_ref(php_libxml_node_object *object, xmlDocPtr docp)
{
int ret_refcount = -1;
diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h
index 54ee2902ca6f..154575baded6 100644
--- a/ext/libxml/php_libxml.h
+++ b/ext/libxml/php_libxml.h
@@ -58,6 +58,9 @@ typedef struct _libxml_doc_props {
bool recover;
} libxml_doc_props;
+/* Modification tracking: when the object changes, we increment its counter.
+ * When this counter no longer matches the counter at the time of caching,
+ * we know that the object has changed and we have to update the cache. */
typedef struct {
size_t modification_nr;
} php_libxml_cache_tag;
@@ -129,23 +132,39 @@ static inline php_libxml_node_object *php_libxml_node_fetch_object(zend_object *
return (php_libxml_node_object *)((char*)(obj) - obj->handlers->offset);
}
-static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_ref_obj *doc_ptr)
+static zend_always_inline void php_libxml_invalidate_cache_tag(php_libxml_cache_tag *cache_tag)
{
- if (!doc_ptr) {
- return;
- }
#if SIZEOF_SIZE_T == 8
/* If one operation happens every nanosecond, then it would still require 584 years to overflow
* the counter. So we'll just assume this never happens. */
- doc_ptr->cache_tag.modification_nr++;
+ cache_tag->modification_nr++;
#else
- size_t new_modification_nr = doc_ptr->cache_tag.modification_nr + 1;
+ size_t new_modification_nr = cache_tag->modification_nr + 1;
if (EXPECTED(new_modification_nr > 0)) { /* unsigned overflow; checking after addition results in one less instruction */
- doc_ptr->cache_tag.modification_nr = new_modification_nr;
+ cache_tag->modification_nr = new_modification_nr;
}
#endif
}
+static zend_always_inline bool php_libxml_is_cache_tag_stale(const php_libxml_cache_tag *object_tag, const php_libxml_cache_tag *cache_tag)
+{
+ ZEND_ASSERT(object_tag != NULL);
+ ZEND_ASSERT(cache_tag != NULL);
+ /* See overflow comment in php_libxml_invalidate_node_list_cache(). */
+#if SIZEOF_SIZE_T == 8
+ return cache_tag->modification_nr != object_tag->modification_nr;
+#else
+ return cache_tag->modification_nr != object_tag->modification_nr || UNEXPECTED(object_tag->modification_nr == SIZE_MAX);
+#endif
+}
+
+static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_ref_obj *doc_ptr)
+{
+ if (doc_ptr) {
+ php_libxml_invalidate_cache_tag(&doc_ptr->cache_tag);
+ }
+}
+
static zend_always_inline void php_libxml_invalidate_node_list_cache_from_doc(xmlDocPtr docp)
{
if (docp && docp->_private) { /* docp is NULL for detached nodes */
@@ -169,6 +188,7 @@ typedef enum {
PHP_LIBXML_API int php_libxml_increment_node_ptr(php_libxml_node_object *object, xmlNodePtr node, void *private_data);
PHP_LIBXML_API int php_libxml_decrement_node_ptr(php_libxml_node_object *object);
+PHP_LIBXML_API int php_libxml_decrement_node_ptr_ref(php_libxml_node_ptr *ptr);
PHP_LIBXML_API int php_libxml_increment_doc_ref(php_libxml_node_object *object, xmlDocPtr docp);
PHP_LIBXML_API int php_libxml_decrement_doc_ref_directly(php_libxml_ref_obj *document);
PHP_LIBXML_API int php_libxml_decrement_doc_ref(php_libxml_node_object *object);