Skip to content

Commit 9121b01

Browse files
authored
1 parent 114fc16 commit 9121b01

12 files changed

+364
-4
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ PHP NEWS
88

99
- DOM:
1010
. Make cloning DOM node lists, maps, and collections fail. (nielsdos)
11+
. Added Dom\Element::getElementsByClassName(). (nielsdos)
1112

1213
- PDO_ODBC
1314
. Fetch larger block sizes and better handle SQL_NO_TOTAL when calling

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ PHP 8.5 UPGRADE NOTES
408408
RFC: https://wiki.php.net/rfc/curl_share_persistence_improvement
409409

410410
- DOM:
411+
. Added Dom\Element::getElementsByClassName().
411412
. Added Dom\Element::insertAdjacentHTML().
412413

413414
- Enchant:

ext/dom/element.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,44 @@ PHP_METHOD(Dom_Element, getElementsByTagName)
842842
}
843843
/* }}} end dom_element_get_elements_by_tag_name */
844844

845+
PHP_METHOD(Dom_Element, getElementsByClassName)
846+
{
847+
dom_object *intern, *namednode;
848+
zend_string *class_names;
849+
850+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "P", &class_names) == FAILURE) {
851+
RETURN_THROWS();
852+
}
853+
854+
if (ZSTR_LEN(class_names) > INT_MAX) {
855+
zend_argument_value_error(1, "is too long");
856+
RETURN_THROWS();
857+
}
858+
859+
DOM_GET_THIS_INTERN(intern);
860+
861+
object_init_ex(return_value, dom_html_collection_class_entry);
862+
namednode = Z_DOMOBJ_P(return_value);
863+
864+
HashTable *token_set;
865+
ALLOC_HASHTABLE(token_set);
866+
zend_hash_init(token_set, 0, NULL, NULL, false);
867+
dom_ordered_set_parser(token_set, ZSTR_VAL(class_names), intern->document->quirks_mode == PHP_LIBXML_QUIRKS);
868+
869+
if (zend_hash_num_elements(token_set) == 0) {
870+
php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_noop);
871+
872+
zend_hash_destroy(token_set);
873+
FREE_HASHTABLE(token_set);
874+
} else {
875+
php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_by_class_name);
876+
877+
dom_nnodemap_object *map = namednode->ptr;
878+
map->array = token_set;
879+
map->release_array = true;
880+
}
881+
}
882+
845883
/* should_free_result must be initialized to false */
846884
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)
847885
{

ext/dom/obj_map.c

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
2525
#include "php_dom.h"
2626
#include "obj_map.h"
27+
#include "token_list.h"
2728

2829
static zend_always_inline void objmap_cache_release_cached_obj(dom_nnodemap_object *objmap)
2930
{
@@ -40,6 +41,30 @@ static zend_always_inline void reset_objmap_cache(dom_nnodemap_object *objmap)
4041
objmap->cached_length = -1;
4142
}
4243

44+
static bool dom_matches_class_name(const dom_nnodemap_object *map, const xmlNode *nodep)
45+
{
46+
bool ret = false;
47+
48+
if (nodep->type == XML_ELEMENT_NODE) {
49+
xmlAttrPtr classes = xmlHasNsProp(nodep, BAD_CAST "class", NULL);
50+
if (classes != NULL) {
51+
bool should_free;
52+
xmlChar *value = php_libxml_attr_value(classes, &should_free);
53+
54+
bool quirks = map->baseobj->document->quirks_mode == PHP_LIBXML_QUIRKS;
55+
if (dom_ordered_set_all_contained(map->array, (const char *) value, quirks)) {
56+
ret = true;
57+
}
58+
59+
if (should_free) {
60+
xmlFree(value);
61+
}
62+
}
63+
}
64+
65+
return ret;
66+
}
67+
4368
/**************************
4469
* === Length methods === *
4570
**************************/
@@ -106,6 +131,24 @@ static zend_long dom_map_get_by_tag_name_length(dom_nnodemap_object *map)
106131
return count;
107132
}
108133

134+
static zend_long dom_map_get_by_class_name_length(dom_nnodemap_object *map)
135+
{
136+
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
137+
zend_long count = 0;
138+
if (nodep) {
139+
xmlNodePtr basep = nodep;
140+
nodep = php_dom_first_child_of_container_node(basep);
141+
142+
while (nodep != NULL) {
143+
if (dom_matches_class_name(map, nodep)) {
144+
count++;
145+
}
146+
nodep = php_dom_next_in_tree_order(nodep, basep);
147+
}
148+
}
149+
return count;
150+
}
151+
109152
static zend_long dom_map_get_zero_length(dom_nnodemap_object *map)
110153
{
111154
return 0;
@@ -276,6 +319,10 @@ static void dom_map_collection_named_item_elements_iter(dom_nnodemap_object *map
276319
}
277320
}
278321

322+
static void dom_map_collection_named_item_null(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter)
323+
{
324+
}
325+
279326
static void dom_map_get_by_tag_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
280327
{
281328
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
292339
}
293340
}
294341

342+
static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
343+
{
344+
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
345+
xmlNodePtr itemnode = NULL;
346+
if (nodep && index >= 0) {
347+
dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index);
348+
if (start_point.node) {
349+
if (start_point.index > 0) {
350+
/* Only start iteration at next point if we actually have an index to seek to. */
351+
itemnode = php_dom_next_in_tree_order(start_point.node, nodep);
352+
} else {
353+
itemnode = start_point.node;
354+
}
355+
} else {
356+
itemnode = php_dom_first_child_of_container_node(nodep);
357+
}
358+
359+
do {
360+
--start_point.index;
361+
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
362+
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
363+
}
364+
} while (start_point.index > 0 && itemnode);
365+
}
366+
dom_ret_node_to_zobj(map, itemnode, return_value);
367+
if (itemnode) {
368+
dom_map_cache_obj(map, itemnode, index, return_value);
369+
}
370+
}
371+
295372
static void dom_map_collection_named_item_by_tag_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter)
296373
{
297374
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);
298375
iter->next = iter->cur + 1;
299376
}
300377

378+
static void dom_map_collection_named_item_by_class_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter)
379+
{
380+
xmlNodePtr basep = iter->basep;
381+
xmlNodePtr nodep = iter->candidate ? php_dom_next_in_tree_order(iter->candidate, basep) : php_dom_first_child_of_container_node(basep);
382+
383+
while (nodep != NULL && !dom_matches_class_name(map, nodep)) {
384+
nodep = php_dom_next_in_tree_order(nodep, basep);
385+
}
386+
387+
iter->candidate = nodep;
388+
}
389+
301390
static void dom_map_get_null_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
302391
{
303392
RETURN_NULL();
@@ -478,6 +567,16 @@ const php_dom_obj_map_handler php_dom_obj_map_by_tag_name = {
478567
.nameless = true,
479568
};
480569

570+
const php_dom_obj_map_handler php_dom_obj_map_by_class_name = {
571+
.length = dom_map_get_by_class_name_length,
572+
.get_item = dom_map_get_by_class_name_item,
573+
.get_ns_named_item = dom_map_get_ns_named_item_null,
574+
.has_ns_named_item = dom_map_has_ns_named_item_null,
575+
.collection_named_item_iter = dom_map_collection_named_item_by_class_name_iter,
576+
.use_cache = true,
577+
.nameless = true,
578+
};
579+
481580
const php_dom_obj_map_handler php_dom_obj_map_child_nodes = {
482581
.length = dom_map_get_nodes_length,
483582
.get_item = dom_map_get_nodes_item,
@@ -533,7 +632,7 @@ const php_dom_obj_map_handler php_dom_obj_map_noop = {
533632
.get_item = dom_map_get_null_item,
534633
.get_ns_named_item = dom_map_get_ns_named_item_null,
535634
.has_ns_named_item = dom_map_has_ns_named_item_null,
536-
.collection_named_item_iter = NULL,
635+
.collection_named_item_iter = dom_map_collection_named_item_null,
537636
.use_cache = false,
538637
.nameless = true,
539638
};

ext/dom/obj_map.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ zend_long php_dom_get_nodelist_length(dom_object *obj);
6363

6464
extern const php_dom_obj_map_handler php_dom_obj_map_attributes;
6565
extern const php_dom_obj_map_handler php_dom_obj_map_by_tag_name;
66+
extern const php_dom_obj_map_handler php_dom_obj_map_by_class_name;
6667
extern const php_dom_obj_map_handler php_dom_obj_map_child_elements;
6768
extern const php_dom_obj_map_handler php_dom_obj_map_child_nodes;
6869
extern const php_dom_obj_map_handler php_dom_obj_map_nodeset;

ext/dom/php_dom.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,7 @@ public function removeAttributeNode(Attr $attr) : Attr {}
16591659

16601660
public function getElementsByTagName(string $qualifiedName): HTMLCollection {}
16611661
public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {}
1662+
public function getElementsByClassName(string $classNames): HTMLCollection {}
16621663

16631664
public function insertAdjacentElement(AdjacentPosition $where, Element $element): ?Element {}
16641665
public function insertAdjacentText(AdjacentPosition $where, string $data): void {}
@@ -1986,6 +1987,8 @@ abstract class Document extends Node implements ParentNode
19861987
public function getElementsByTagName(string $qualifiedName): HTMLCollection {}
19871988
/** @implementation-alias Dom\Element::getElementsByTagNameNS */
19881989
public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {}
1990+
/** @implementation-alias Dom\Element::getElementsByClassName */
1991+
public function getElementsByClassName(string $classNames): HTMLCollection {}
19891992

19901993
public function createElement(string $localName): Element {}
19911994
public function createElementNS(?string $namespace, string $qualifiedName): Element {}

ext/dom/php_dom_arginfo.h

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Dom\Element::getElementsByClassName() empty class names
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$dom = Dom\HTMLDocument::createFromString(<<<HTML
9+
<div class=" foo bar ">
10+
<p id="child"></p>
11+
</div>
12+
HTML, LIBXML_NOERROR);
13+
14+
$collection = $dom->documentElement->getElementsByClassName("");
15+
var_dump($collection->count());
16+
17+
foreach ($collection as $node) {
18+
throw new Error("unreachable");
19+
}
20+
21+
var_dump($dom->getElementsByClassName(" ")->count());
22+
var_dump($dom->getElementsByClassName("\t")->count());
23+
var_dump($dom->getElementsByClassName("\t\n\f\v")->count());
24+
var_dump($dom->getElementsByClassName("\t\n\f\v")->namedItem("child"));
25+
26+
?>
27+
--EXPECT--
28+
int(0)
29+
int(0)
30+
int(0)
31+
int(0)
32+
NULL
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Dom\Element::getElementsByClassName() non quirks mode
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$dom = Dom\HTMLDocument::createFromString(<<<HTML
9+
<!DOCTYPE html>
10+
<div id="container">
11+
<p class="Bar">1</p>
12+
<p class="bar">2</p>
13+
<p class="Bar Foo">3</p>
14+
<p class="Bar foo">4</p>
15+
<p class="foo bar">5</p>
16+
<p class="foo bar" name="here">6</p>
17+
</div>
18+
<div>
19+
<p class="Bar">7</p>
20+
<p class="bar">8</p>
21+
<p class="Bar Foo">9</p>
22+
<p class="Bar foo">10</p>
23+
<p class="foo bar">11</p>
24+
</div>
25+
HTML);
26+
27+
$collection = $dom->getElementsByClassName("foo \n bar");
28+
29+
echo "There are {$collection->length} items in the document in total that have both \"foo\" and \"bar\"\n";
30+
31+
$collection = $dom->getElementById('container')->getElementsByClassName("foo \n bar");
32+
33+
echo "There are {$collection->length} items in #container in total that have both \"foo\" and \"bar\"\n";
34+
35+
foreach ($collection as $key => $node) {
36+
echo "--- Key $key ---\n";
37+
var_dump($node->tagName, $node->textContent);
38+
var_dump($node === $collection->item($key));
39+
}
40+
41+
var_dump($collection->namedItem("here")->textContent);
42+
43+
?>
44+
--EXPECT--
45+
There are 3 items in the document in total that have both "foo" and "bar"
46+
There are 2 items in #container in total that have both "foo" and "bar"
47+
--- Key 0 ---
48+
string(1) "P"
49+
string(1) "5"
50+
bool(true)
51+
--- Key 1 ---
52+
string(1) "P"
53+
string(1) "6"
54+
bool(true)
55+
string(1) "6"

0 commit comments

Comments
 (0)