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);