1
+ +3
+4
+diff --git a/NEWS b/NEWS index b12686c5d3cef..7df251f85508d 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ PHP NEWS - DOM: . Make cloning DOM node lists, maps, and collections fail. (nielsdos) + . Added Dom\Element::getElementsByClassName(). (nielsdos) - PDO_ODBC . Fetch larger block sizes and better handle SQL_NO_TOTAL when calling diff --git a/UPGRADING b/UPGRADING index fb76c9a46ba0e..e96b92683caa1 100644 --- a/UPGRADING +++ b/UPGRADING @@ -408,6 +408,7 @@ PHP 8.5 UPGRADE NOTES RFC: https://wiki.php.net/rfc/curl_share_persistence_improvement - DOM: + . Added Dom\Element::getElementsByClassName(). . Added Dom\Element::insertAdjacentHTML(). - Enchant: diff --git a/ext/dom/element.c b/ext/dom/element.c index ea18d0a817ede..f0afcf07eb2fe 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -842,6 +842,44 @@ PHP_METHOD(Dom_Element, getElementsByTagName) } /* }}} end dom_element_get_elements_by_tag_name */ +PHP_METHOD(Dom_Element, getElementsByClassName) +{ + dom_object *intern, *namednode; + zend_string *class_names; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "P", &class_names) == FAILURE) { + RETURN_THROWS(); + } + + if (ZSTR_LEN(class_names) > INT_MAX) { + zend_argument_value_error(1, "is too long"); + RETURN_THROWS(); + } + + DOM_GET_THIS_INTERN(intern); + + object_init_ex(return_value, dom_html_collection_class_entry); + namednode = Z_DOMOBJ_P(return_value); + + HashTable *token_set; + ALLOC_HASHTABLE(token_set); + zend_hash_init(token_set, 0, NULL, NULL, false); + dom_ordered_set_parser(token_set, ZSTR_VAL(class_names), intern->document->quirks_mode == PHP_LIBXML_QUIRKS); + + if (zend_hash_num_elements(token_set) == 0) { + php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_noop); + + zend_hash_destroy(token_set); + FREE_HASHTABLE(token_set); + } else { + php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_by_class_name); + + dom_nnodemap_object *map = namednode->ptr; + map->array = token_set; + map->release_array = true; + } +} + /* should_free_result must be initialized to false */ static const xmlChar *dom_get_attribute_ns(dom_object *intern, xmlNodePtr elemp, const char *uri, size_t uri_len, const char *name, bool *should_free_result) { diff --git a/ext/dom/obj_map.c b/ext/dom/obj_map.c index 64e4cff7b6ffe..9c017c4d11a6b 100644 --- a/ext/dom/obj_map.c +++ b/ext/dom/obj_map.c @@ -24,6 +24,7 @@ #if defined(HAVE_LIBXML) && defined(HAVE_DOM) #include "php_dom.h" #include "obj_map.h" +#include "token_list.h" static zend_always_inline void objmap_cache_release_cached_obj(dom_nnodemap_object *objmap) { @@ -40,6 +41,30 @@ static zend_always_inline void reset_objmap_cache(dom_nnodemap_object *objmap) objmap->cached_length = -1; } +static bool dom_matches_class_name(const dom_nnodemap_object *map, const xmlNode *nodep) +{ + bool ret = false; + + if (nodep->type == XML_ELEMENT_NODE) { + xmlAttrPtr classes = xmlHasNsProp(nodep, BAD_CAST "class", NULL); + if (classes != NULL) { + bool should_free; + xmlChar *value = php_libxml_attr_value(classes, &should_free); + + bool quirks = map->baseobj->document->quirks_mode == PHP_LIBXML_QUIRKS; + if (dom_ordered_set_all_contained(map->array, (const char *) value, quirks)) { + ret = true; + } + + if (should_free) { + xmlFree(value); + } + } + } + + return ret; +} + /************************** * === Length methods === * **************************/ @@ -106,6 +131,24 @@ static zend_long dom_map_get_by_tag_name_length(dom_nnodemap_object *map) return count; } +static zend_long dom_map_get_by_class_name_length(dom_nnodemap_object *map) +{ + xmlNodePtr nodep = dom_object_get_node(map->baseobj); + zend_long count = 0; + if (nodep) { + xmlNodePtr basep = nodep; + nodep = php_dom_first_child_of_container_node(basep); + + while (nodep != NULL) { + if (dom_matches_class_name(map, nodep)) { + count++; + } + nodep = php_dom_next_in_tree_order(nodep, basep); + } + } + return count; +} + static zend_long dom_map_get_zero_length(dom_nnodemap_object *map) { return 0; @@ -276,6 +319,10 @@ static void dom_map_collection_named_item_elements_iter(dom_nnodemap_object *map } } +static void dom_map_collection_named_item_null(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter) +{ +} + static void dom_map_get_by_tag_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value) { xmlNodePtr nodep = dom_object_get_node(map->baseobj); @@ -292,12 +339,54 @@ static void dom_map_get_by_tag_name_item(dom_nnodemap_object *map, zend_long ind } } +static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value) +{ + xmlNodePtr nodep = dom_object_get_node(map->baseobj); + xmlNodePtr itemnode = NULL; + if (nodep && index >= 0) { + dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index); + if (start_point.node) { + if (start_point.index > 0) { + /* Only start iteration at next point if we actually have an index to seek to. */ + itemnode = php_dom_next_in_tree_order(start_point.node, nodep); + } else { + itemnode = start_point.node; + } + } else { + itemnode = php_dom_first_child_of_container_node(nodep); + } + + do { + --start_point.index; + while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) { + itemnode = php_dom_next_in_tree_order(itemnode, nodep); + } + } while (start_point.index > 0 && itemnode); + } + dom_ret_node_to_zobj(map, itemnode, return_value); + if (itemnode) { + dom_map_cache_obj(map, itemnode, index, return_value); + } +} + static void dom_map_collection_named_item_by_tag_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter) { iter->candidate = dom_get_elements_by_tag_name_ns_raw(iter->basep, iter->candidate, map->ns, map->local, map->local_lower, &iter->cur, iter->next); iter->next = iter->cur + 1; } +static void dom_map_collection_named_item_by_class_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter) +{ + xmlNodePtr basep = iter->basep; + xmlNodePtr nodep = iter->candidate ? php_dom_next_in_tree_order(iter->candidate, basep) : php_dom_first_child_of_container_node(basep); + + while (nodep != NULL && !dom_matches_class_name(map, nodep)) { + nodep = php_dom_next_in_tree_order(nodep, basep); + } + + iter->candidate = nodep; +} + static void dom_map_get_null_item(dom_nnodemap_object *map, zend_long index, zval *return_value) { RETURN_NULL(); @@ -478,6 +567,16 @@ const php_dom_obj_map_handler php_dom_obj_map_by_tag_name = { .nameless = true, }; +const php_dom_obj_map_handler php_dom_obj_map_by_class_name = { + .length = dom_map_get_by_class_name_length, + .get_item = dom_map_get_by_class_name_item, + .get_ns_named_item = dom_map_get_ns_named_item_null, + .has_ns_named_item = dom_map_has_ns_named_item_null, + .collection_named_item_iter = dom_map_collection_named_item_by_class_name_iter, + .use_cache = true, + .nameless = true, +}; + const php_dom_obj_map_handler php_dom_obj_map_child_nodes = { .length = dom_map_get_nodes_length, .get_item = dom_map_get_nodes_item, @@ -533,7 +632,7 @@ const php_dom_obj_map_handler php_dom_obj_map_noop = { .get_item = dom_map_get_null_item, .get_ns_named_item = dom_map_get_ns_named_item_null, .has_ns_named_item = dom_map_has_ns_named_item_null, - .collection_named_item_iter = NULL, + .collection_named_item_iter = dom_map_collection_named_item_null, .use_cache = false, .nameless = true, }; diff --git a/ext/dom/obj_map.h b/ext/dom/obj_map.h index 97ec43f0011f8..e7231eb7b10a8 100644 --- a/ext/dom/obj_map.h +++ b/ext/dom/obj_map.h @@ -63,6 +63,7 @@ zend_long php_dom_get_nodelist_length(dom_object *obj); extern const php_dom_obj_map_handler php_dom_obj_map_attributes; extern const php_dom_obj_map_handler php_dom_obj_map_by_tag_name; +extern const php_dom_obj_map_handler php_dom_obj_map_by_class_name; extern const php_dom_obj_map_handler php_dom_obj_map_child_elements; extern const php_dom_obj_map_handler php_dom_obj_map_child_nodes; extern const php_dom_obj_map_handler php_dom_obj_map_nodeset; diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index fd0927fc0ea12..7236e80b4d247 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -1659,6 +1659,7 @@ public function removeAttributeNode(Attr $attr) : Attr {} public function getElementsByTagName(string $qualifiedName): HTMLCollection {} public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {} + public function getElementsByClassName(string $classNames): HTMLCollection {} public function insertAdjacentElement(AdjacentPosition $where, Element $element): ?Element {} public function insertAdjacentText(AdjacentPosition $where, string $data): void {} @@ -1986,6 +1987,8 @@ abstract class Document extends Node implements ParentNode public function getElementsByTagName(string $qualifiedName): HTMLCollection {} /** @implementation-alias Dom\Element::getElementsByTagNameNS */ public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {} + /** @implementation-alias Dom\Element::getElementsByClassName */ + public function getElementsByClassName(string $classNames): HTMLCollection {} public function createElement(string $localName): Element {} public function createElementNS(?string $namespace, string $qualifiedName): Element {} diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index be7a5c0713ad4..73efc8696e92f 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: 2119512797f6d51d9835660cd0eccd3ba83417a9 */ + * Stub hash: 757889c0ca89cc8e9905ba465e0621fe89b6e716 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_dom_import_simplexml, 0, 1, DOMAttr|DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -775,6 +775,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_Element_getElementsByTa ZEND_ARG_TYPE_INFO(0, localName, IS_STRING, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_Element_getElementsByClassName, 0, 1, Dom\\HTMLCollection, 0) + ZEND_ARG_TYPE_INFO(0, classNames, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_Element_insertAdjacentElement, 0, 2, Dom\\Element, 1) ZEND_ARG_OBJ_INFO(0, where, Dom\\AdjacentPosition, 0) ZEND_ARG_OBJ_INFO(0, element, Dom\\Element, 0) @@ -906,6 +910,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_Dom_Document_getElementsByTagNameNS arginfo_class_Dom_Element_getElementsByTagNameNS +#define arginfo_class_Dom_Document_getElementsByClassName arginfo_class_Dom_Element_getElementsByClassName + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_Document_createElement, 0, 1, Dom\\Element, 0) ZEND_ARG_TYPE_INFO(0, localName, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -1278,6 +1284,7 @@ ZEND_METHOD(Dom_Element, setAttributeNodeNS); ZEND_METHOD(Dom_Element, removeAttributeNode); ZEND_METHOD(Dom_Element, getElementsByTagName); ZEND_METHOD(Dom_Element, getElementsByTagNameNS); +ZEND_METHOD(Dom_Element, getElementsByClassName); ZEND_METHOD(Dom_Element, insertAdjacentElement); ZEND_METHOD(Dom_Element, insertAdjacentText); ZEND_METHOD(Dom_Element, insertAdjacentHTML); @@ -1653,6 +1660,7 @@ static const zend_function_entry class_Dom_Element_methods[] = { ZEND_ME(Dom_Element, removeAttributeNode, arginfo_class_Dom_Element_removeAttributeNode, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, getElementsByTagName, arginfo_class_Dom_Element_getElementsByTagName, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, getElementsByTagNameNS, arginfo_class_Dom_Element_getElementsByTagNameNS, ZEND_ACC_PUBLIC) + ZEND_ME(Dom_Element, getElementsByClassName, arginfo_class_Dom_Element_getElementsByClassName, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, insertAdjacentElement, arginfo_class_Dom_Element_insertAdjacentElement, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, insertAdjacentText, arginfo_class_Dom_Element_insertAdjacentText, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Element, insertAdjacentHTML, arginfo_class_Dom_Element_insertAdjacentHTML, ZEND_ACC_PUBLIC) @@ -1721,6 +1729,7 @@ static const zend_function_entry class_Dom_DocumentFragment_methods[] = { static const zend_function_entry class_Dom_Document_methods[] = { ZEND_RAW_FENTRY("getElementsByTagName", zim_Dom_Element_getElementsByTagName, arginfo_class_Dom_Document_getElementsByTagName, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_RAW_FENTRY("getElementsByTagNameNS", zim_Dom_Element_getElementsByTagNameNS, arginfo_class_Dom_Document_getElementsByTagNameNS, ZEND_ACC_PUBLIC, NULL, NULL) + ZEND_RAW_FENTRY("getElementsByClassName", zim_Dom_Element_getElementsByClassName, arginfo_class_Dom_Document_getElementsByClassName, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_ME(Dom_Document, createElement, arginfo_class_Dom_Document_createElement, ZEND_ACC_PUBLIC) ZEND_ME(Dom_Document, createElementNS, arginfo_class_Dom_Document_createElementNS, ZEND_ACC_PUBLIC) ZEND_RAW_FENTRY("createDocumentFragment", zim_DOMDocument_createDocumentFragment, arginfo_class_Dom_Document_createDocumentFragment, ZEND_ACC_PUBLIC, NULL, NULL) diff --git a/ext/dom/tests/modern/common/Element_getElementsByClassName_empty.phpt b/ext/dom/tests/modern/common/Element_getElementsByClassName_empty.phpt new file mode 100644 index 0000000000000..d2e3b0ea22b9b --- /dev/null +++ b/ext/dom/tests/modern/common/Element_getElementsByClassName_empty.phpt @@ -0,0 +1,32 @@ +--TEST-- +Dom\Element::getElementsByClassName() empty class names +--EXTENSIONS-- +dom +--FILE-- + +
+ +HTML, LIBXML_NOERROR); + +$collection = $dom->documentElement->getElementsByClassName(""); +var_dump($collection->count()); + +foreach ($collection as $node) { + throw new Error("unreachable"); +} + +var_dump($dom->getElementsByClassName(" ")->count()); +var_dump($dom->getElementsByClassName("\t")->count()); +var_dump($dom->getElementsByClassName("\t\n\f\v")->count()); +var_dump($dom->getElementsByClassName("\t\n\f\v")->namedItem("child")); + +?> +--EXPECT-- +int(0) +int(0) +int(0) +int(0) +NULL diff --git a/ext/dom/tests/modern/common/Element_getElementsByClassName_non_quirks.phpt b/ext/dom/tests/modern/common/Element_getElementsByClassName_non_quirks.phpt new file mode 100644 index 0000000000000..e658e3399ca0d --- /dev/null +++ b/ext/dom/tests/modern/common/Element_getElementsByClassName_non_quirks.phpt @@ -0,0 +1,55 @@ +--TEST-- +Dom\Element::getElementsByClassName() non quirks mode +--EXTENSIONS-- +dom +--FILE-- + +1
+ +3
+4
+ + +7
+ +9
+10
+ +1
+ +3
+4
+