xref: /php-src/ext/dom/token_list.c (revision fc09f4b2)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Niels Dossche <nielsdos@php.net>                            |
14    +----------------------------------------------------------------------+
15 */
16 
17 #ifdef HAVE_CONFIG_H
18 #include <config.h>
19 #endif
20 
21 #include "php.h"
22 #if defined(HAVE_LIBXML) && defined(HAVE_DOM)
23 #include "php_dom.h"
24 #include "token_list.h"
25 #include "infra.h"
26 #include "zend_interfaces.h"
27 
28 #define TOKEN_LIST_GET_INTERNAL() php_dom_token_list_from_obj(Z_OBJ_P(ZEND_THIS))
29 #define TOKEN_LIST_GET_SET(intern) (&(intern)->token_set)
30 #define Z_TOKEN_LIST_P(zv) php_dom_token_list_from_obj(Z_OBJ_P(zv))
31 
32 typedef struct dom_token_list_it {
33 	zend_object_iterator it;
34 	/* Store the hash position here to allow multiple (e.g. nested) iterations of the same token list. */
35 	HashPosition pos;
36 	php_libxml_cache_tag cache_tag;
37 } dom_token_list_it;
38 
dom_contains_ascii_whitespace(const char * data)39 static zend_always_inline bool dom_contains_ascii_whitespace(const char *data)
40 {
41 	return strpbrk(data, ascii_whitespace) != NULL;
42 }
43 
dom_add_token(HashTable * ht,zend_string * token)44 static zend_always_inline void dom_add_token(HashTable *ht, zend_string *token)
45 {
46 	/* Key outlives the value's lifetime because as long as the entry is in the table it is kept alive. */
47 	zval zv;
48 	ZVAL_STR(&zv, token);
49 	zend_hash_add(ht, token, &zv);
50 }
51 
52 /* https://dom.spec.whatwg.org/#concept-ordered-set-parser
53  * and https://infra.spec.whatwg.org/#split-on-ascii-whitespace */
dom_ordered_set_parser(HashTable * token_set,const char * position)54 static void dom_ordered_set_parser(HashTable *token_set, const char *position)
55 {
56 	/* Adapted steps from "split on ASCII whitespace" such that that loop directly appends to the token set. */
57 
58 	/* 1. Let position be a position variable for input, initially pointing at the start of input.
59 	 *    => That's the position pointer. */
60 	/* 2. Let tokens be a list of strings, initially empty.
61 	 *    => That's the token set. */
62 
63 	/* 3. Skip ASCII whitespace within input given position. */
64 	position += strspn(position, ascii_whitespace);
65 
66 	/* 4. While position is not past the end of input: */
67 	while (*position != '\0') {
68 		/* 4.1. Let token be the result of collecting a sequence of code points that are not ASCII whitespace from input */
69 		const char *start = position;
70 		position += strcspn(position, ascii_whitespace);
71 		size_t length = position - start;
72 
73 		/* 4.2. Append token to tokens. */
74 		zend_string *token = zend_string_init(start, length, false);
75 		dom_add_token(token_set, token);
76 		zend_string_release_ex(token, false);
77 
78 		/* 4.3. Skip ASCII whitespace within input given position. */
79 		position += strspn(position, ascii_whitespace);
80 	}
81 
82 	/* 5. Return tokens.
83 	 *    => That's the token set. */
84 }
85 
86 /* https://dom.spec.whatwg.org/#concept-ordered-set-serializer */
dom_ordered_set_serializer(HashTable * token_set)87 static char *dom_ordered_set_serializer(HashTable *token_set)
88 {
89 	size_t length = 0;
90 	zend_string *token;
91 	ZEND_HASH_MAP_FOREACH_STR_KEY(token_set, token) {
92 		size_t needed_size = ZSTR_LEN(token) + 1; /* +1 for the space (or \0 at the end) */
93 		if (UNEXPECTED(ZSTR_MAX_LEN - length < needed_size)) {
94 			/* Shouldn't really be able to happen in practice. */
95 			zend_throw_error(NULL, "Token set too large");
96 			return NULL;
97 		}
98 		length += needed_size;
99 	} ZEND_HASH_FOREACH_END();
100 
101 	if (length == 0) {
102 		char *ret = emalloc(1);
103 		*ret = '\0';
104 		return ret;
105 	}
106 
107 	char *ret = emalloc(length);
108 	char *ptr = ret;
109 	ZEND_HASH_MAP_FOREACH_STR_KEY(token_set, token) {
110 		memcpy(ptr, ZSTR_VAL(token), ZSTR_LEN(token));
111 		ptr += ZSTR_LEN(token);
112 		*ptr++ = ' ';
113 	} ZEND_HASH_FOREACH_END();
114 	ptr[-1] = '\0'; /* replace last space with \0 */
115 	return ret;
116 }
117 
dom_token_list_get_element(dom_token_list_object * intern)118 static zend_always_inline xmlNode *dom_token_list_get_element(dom_token_list_object *intern)
119 {
120 	php_libxml_node_ptr *element_ptr = intern->dom.ptr;
121 	return element_ptr->node;
122 }
123 
dom_token_list_get_attr(dom_token_list_object * intern)124 static zend_always_inline const xmlAttr *dom_token_list_get_attr(dom_token_list_object *intern)
125 {
126 	const xmlNode *element_node = dom_token_list_get_element(intern);
127 	return xmlHasNsProp(element_node, BAD_CAST "class", NULL);
128 }
129 
130 /* https://dom.spec.whatwg.org/#concept-dtl-update */
dom_token_list_update(dom_token_list_object * intern)131 static void dom_token_list_update(dom_token_list_object *intern)
132 {
133 	const xmlAttr *attr = dom_token_list_get_attr(intern);
134 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
135 
136 	php_libxml_invalidate_cache_tag(&intern->cache_tag);
137 
138 	/* 1. If the associated element does not have an associated attribute and token set is empty, then return. */
139 	if (attr == NULL && zend_hash_num_elements(token_set) == 0) {
140 		return;
141 	}
142 
143 	/* 2. Set an attribute value for the associated element using associated attribute’s local name and the result of
144 	 *    running the ordered set serializer for token set. */
145 	char *value = dom_ordered_set_serializer(token_set);
146 	xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST value);
147 	efree(intern->cached_string);
148 	intern->cached_string = value;
149 }
150 
dom_token_list_get_class_value(const xmlAttr * attr,bool * should_free)151 static xmlChar *dom_token_list_get_class_value(const xmlAttr *attr, bool *should_free)
152 {
153 	if (attr != NULL && attr->children != NULL) {
154 		return php_libxml_attr_value(attr, should_free);
155 	}
156 	*should_free = false;
157 	return NULL;
158 }
159 
dom_token_list_update_set(dom_token_list_object * intern,HashTable * token_set)160 static void dom_token_list_update_set(dom_token_list_object *intern, HashTable *token_set)
161 {
162 	/* https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A1 */
163 	bool should_free;
164 	const xmlAttr *attr = dom_token_list_get_attr(intern);
165 	/* 1. If the data is null, the token set remains empty. */
166 	xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
167 	if (value != NULL) {
168 		/* 2. Otherwise, parse the token set. */
169 		dom_ordered_set_parser(token_set, (const char *) value);
170 		intern->cached_string = estrdup((const char *) value);
171 	} else {
172 		intern->cached_string = NULL;
173 	}
174 
175 	if (should_free) {
176 		xmlFree(value);
177 	}
178 }
179 
dom_token_list_ensure_set_up_to_date(dom_token_list_object * intern)180 static void dom_token_list_ensure_set_up_to_date(dom_token_list_object *intern)
181 {
182 	bool should_free;
183 	const xmlAttr *attr = dom_token_list_get_attr(intern);
184 	xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
185 
186 	/* xmlStrEqual will automatically handle equality rules of NULL vs "" (etc.) correctly. */
187 	if (!xmlStrEqual(value, (const xmlChar *) intern->cached_string)) {
188 		php_libxml_invalidate_cache_tag(&intern->cache_tag);
189 		efree(intern->cached_string);
190 		HashTable *token_set = TOKEN_LIST_GET_SET(intern);
191 		zend_hash_destroy(token_set);
192 		zend_hash_init(token_set, 0, NULL, NULL, false);
193 		dom_token_list_update_set(intern, token_set);
194 	}
195 
196 	if (should_free) {
197 		xmlFree(value);
198 	}
199 }
200 
dom_token_list_ctor(dom_token_list_object * intern,dom_object * element_obj)201 void dom_token_list_ctor(dom_token_list_object *intern, dom_object *element_obj)
202 {
203 	php_libxml_node_ptr *ptr = element_obj->ptr;
204 	ptr->refcount++;
205 	intern->dom.ptr = ptr;
206 	element_obj->document->refcount++;
207 	intern->dom.document = element_obj->document;
208 
209 	intern->cache_tag.modification_nr = 0;
210 
211 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
212 	zend_hash_init(token_set, 0, NULL, NULL, false);
213 
214 	dom_token_list_update_set(intern, token_set);
215 }
216 
dom_token_list_free_obj(zend_object * object)217 void dom_token_list_free_obj(zend_object *object)
218 {
219 	dom_token_list_object *intern = php_dom_token_list_from_obj(object);
220 
221 	zend_object_std_dtor(object);
222 
223 	if (EXPECTED(intern->dom.ptr != NULL)) { /* Object initialized? */
224 		xmlNodePtr node = dom_token_list_get_element(intern);
225 		if (php_libxml_decrement_node_ptr_ref(intern->dom.ptr) == 0) {
226 			php_libxml_node_free_resource(node);
227 		}
228 		php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
229 		HashTable *token_set = TOKEN_LIST_GET_SET(intern);
230 		zend_hash_destroy(token_set);
231 		efree(intern->cached_string);
232 	}
233 }
234 
dom_token_list_item_exists(dom_token_list_object * token_list,zend_long index)235 static bool dom_token_list_item_exists(dom_token_list_object *token_list, zend_long index)
236 {
237 	dom_token_list_ensure_set_up_to_date(token_list);
238 
239 	HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
240 	return index >= 0 && index < zend_hash_num_elements(token_set);
241 }
242 
dom_token_list_item_read(dom_token_list_object * token_list,zval * retval,zend_long index)243 static void dom_token_list_item_read(dom_token_list_object *token_list, zval *retval, zend_long index)
244 {
245 	dom_token_list_ensure_set_up_to_date(token_list);
246 
247 	HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
248 	if (index >= 0 && index < zend_hash_num_elements(token_set)) {
249 		HashPosition position;
250 		zend_hash_internal_pointer_reset_ex(token_set, &position);
251 		while (index > 0) {
252 			zend_hash_move_forward_ex(token_set, &position);
253 			index--;
254 		}
255 		zend_string *str_index;
256 		zend_hash_get_current_key_ex(token_set, &str_index, NULL, &position);
257 		ZVAL_STR_COPY(retval, str_index);
258 	} else {
259 		/* Not an out of bounds ValueError, but NULL, as according to spec.
260 		 * This design choice allows for constructs like `item(x) ?? ...`
261 		 *
262 		 * In particular:
263 		 * https://dom.spec.whatwg.org/#interface-domtokenlist states DOMTokenList implements iterable<DOMString>.
264 		 * From https://webidl.spec.whatwg.org/#idl-iterable:
265 		 *   If a single type parameter is given,
266 		 *   then the interface has a value iterator and provides values of the specified type.
267 		 * This applies, and reading the definition of value iterator means we should support indexed properties.
268 		 * From https://webidl.spec.whatwg.org/#dfn-support-indexed-properties:
269 		 *   An interface that defines an indexed property getter is said to support indexed properties.
270 		 * And indexed property getter is defined here: https://webidl.spec.whatwg.org/#dfn-indexed-property-getter
271 		 * Down below in their note they give an example of how an out-of-bounds access evaluates to undefined,
272 		 * which would map to NULL for us.
273 		 * This would also be consistent with how out-of-bounds array accesses in PHP result in NULL. */
274 		ZVAL_NULL(retval);
275 	}
276 }
277 
278 /* Adapted from spl_offset_convert_to_long */
dom_token_list_offset_convert_to_long(zval * offset,bool * failed)279 static zend_long dom_token_list_offset_convert_to_long(zval *offset, bool *failed)
280 {
281 	*failed = false;
282 
283 	while (true) {
284 		switch (Z_TYPE_P(offset)) {
285 			case IS_STRING: {
286 				zend_ulong index;
287 				if (ZEND_HANDLE_NUMERIC(Z_STR_P(offset), index)) {
288 					return (zend_long) index;
289 				}
290 				ZEND_FALLTHROUGH;
291 			}
292 			default:
293 				*failed = true;
294 				return 0;
295 			case IS_DOUBLE:
296 				return zend_dval_to_lval_safe(Z_DVAL_P(offset));
297 			case IS_LONG:
298 				return Z_LVAL_P(offset);
299 			case IS_FALSE:
300 				return 0;
301 			case IS_TRUE:
302 				return 1;
303 			case IS_REFERENCE:
304 				offset = Z_REFVAL_P(offset);
305 				break;
306 			case IS_RESOURCE:
307 				zend_use_resource_as_offset(offset);
308 				return Z_RES_HANDLE_P(offset);
309 		}
310 	}
311 }
312 
dom_token_list_read_dimension(zend_object * object,zval * offset,int type,zval * rv)313 zval *dom_token_list_read_dimension(zend_object *object, zval *offset, int type, zval *rv)
314 {
315 	if (!offset) {
316 		zend_throw_error(NULL, "Cannot append to Dom\\TokenList");
317 		return NULL;
318 	}
319 
320 	bool failed;
321 	zend_long index = dom_token_list_offset_convert_to_long(offset, &failed);
322 	if (UNEXPECTED(failed)) {
323 		zend_illegal_container_offset(object->ce->name, offset, type);
324 		return NULL;
325 	} else {
326 		dom_token_list_item_read(php_dom_token_list_from_obj(object), rv, index);
327 		return rv;
328 	}
329 }
330 
dom_token_list_has_dimension(zend_object * object,zval * offset,int check_empty)331 int dom_token_list_has_dimension(zend_object *object, zval *offset, int check_empty)
332 {
333 	bool failed;
334 	zend_long index = dom_token_list_offset_convert_to_long(offset, &failed);
335 	if (UNEXPECTED(failed)) {
336 		zend_illegal_container_offset(object->ce->name, offset, BP_VAR_IS);
337 		return 0;
338 	} else {
339 		dom_token_list_object *token_list = php_dom_token_list_from_obj(object);
340 		if (check_empty) {
341 			/* Need to perform an actual read to have the correct empty() semantics. */
342 			zval rv;
343 			dom_token_list_item_read(token_list, &rv, index);
344 			int is_true = zend_is_true(&rv);
345 			zval_ptr_dtor_nogc(&rv);
346 			return is_true;
347 		} else {
348 			return dom_token_list_item_exists(token_list, index);
349 		}
350 	}
351 }
352 
353 /* https://dom.spec.whatwg.org/#dom-domtokenlist-length */
dom_token_list_length_read(dom_object * obj,zval * retval)354 zend_result dom_token_list_length_read(dom_object *obj, zval *retval)
355 {
356 	dom_token_list_object *token_list = php_dom_token_list_from_dom_obj(obj);
357 	dom_token_list_ensure_set_up_to_date(token_list);
358 	ZVAL_LONG(retval, zend_hash_num_elements(TOKEN_LIST_GET_SET(token_list)));
359 	return SUCCESS;
360 }
361 
362 /* https://dom.spec.whatwg.org/#dom-domtokenlist-value
363  * and https://dom.spec.whatwg.org/#concept-dtl-serialize */
dom_token_list_value_read(dom_object * obj,zval * retval)364 zend_result dom_token_list_value_read(dom_object *obj, zval *retval)
365 {
366 	bool should_free;
367 	dom_token_list_object *intern = php_dom_token_list_from_dom_obj(obj);
368 	const xmlAttr *attr = dom_token_list_get_attr(intern);
369 	xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
370 	ZVAL_STRING(retval, value ? (const char *) value : "");
371 	if (should_free) {
372 		xmlFree(value);
373 	}
374 	return SUCCESS;
375 }
376 
377 /* https://dom.spec.whatwg.org/#dom-domtokenlist-value */
dom_token_list_value_write(dom_object * obj,zval * newval)378 zend_result dom_token_list_value_write(dom_object *obj, zval *newval)
379 {
380 	dom_token_list_object *intern = php_dom_token_list_from_dom_obj(obj);
381 	if (UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(newval)))) {
382 		zend_value_error("Value must not contain any null bytes");
383 		return FAILURE;
384 	}
385 	xmlSetNsProp(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST Z_STRVAL_P(newval));
386 	/* Note: we don't update the set here, the set is always lazily updated for performance reasons. */
387 	return SUCCESS;
388 }
389 
390 /* https://dom.spec.whatwg.org/#dom-domtokenlist-item */
PHP_METHOD(Dom_TokenList,item)391 PHP_METHOD(Dom_TokenList, item)
392 {
393 	zend_long index;
394 	ZEND_PARSE_PARAMETERS_START(1, 1)
395 		Z_PARAM_LONG(index)
396 	ZEND_PARSE_PARAMETERS_END();
397 
398 	/* 1. If index is equal to or greater than this’s token set’s size, then return null. */
399 	/* 2. Return this’s token set[index]. */
400 	dom_token_list_item_read(TOKEN_LIST_GET_INTERNAL(), return_value, index);
401 }
402 
403 /* https://dom.spec.whatwg.org/#dom-domtokenlist-contains */
PHP_METHOD(Dom_TokenList,contains)404 PHP_METHOD(Dom_TokenList, contains)
405 {
406 	zend_string *token;
407 	ZEND_PARSE_PARAMETERS_START(1, 1)
408 		Z_PARAM_PATH_STR(token)
409 	ZEND_PARSE_PARAMETERS_END();
410 
411 	dom_token_list_object *token_list = TOKEN_LIST_GET_INTERNAL();
412 	dom_token_list_ensure_set_up_to_date(token_list);
413 	HashTable *token_set = TOKEN_LIST_GET_SET(token_list);
414 	RETURN_BOOL(zend_hash_exists(token_set, token));
415 }
416 
417 /* Steps taken from the add, remove, toggle, replace methods. */
dom_validate_token(const zend_string * str)418 static bool dom_validate_token(const zend_string *str)
419 {
420 	/* 1. If token is the empty string, then throw a "SyntaxError" DOMException. */
421 	if (ZSTR_LEN(str) == 0) {
422 		php_dom_throw_error_with_message(SYNTAX_ERR, "The empty string is not a valid token", true);
423 		return false;
424 	}
425 
426 	/* 2. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. */
427 	if (dom_contains_ascii_whitespace(ZSTR_VAL(str))) {
428 		php_dom_throw_error_with_message(INVALID_CHARACTER_ERR, "The token must not contain any ASCII whitespace", true);
429 		return false;
430 	}
431 
432 	return true;
433 }
434 
dom_validate_tokens_varargs(const zval * args,uint32_t argc)435 static bool dom_validate_tokens_varargs(const zval *args, uint32_t argc)
436 {
437 	for (uint32_t i = 0; i < argc; i++) {
438 		if (Z_TYPE(args[i]) != IS_STRING) {
439 			zend_argument_type_error(i + 1, "must be of type string, %s given", zend_zval_value_name(&args[i]));
440 			return false;
441 		}
442 
443 		if (zend_str_has_nul_byte(Z_STR(args[i]))) {
444 			zend_argument_value_error(i + 1, "must not contain any null bytes");
445 			return false;
446 		}
447 
448 		if (!dom_validate_token(Z_STR(args[i]))) {
449 			return false;
450 		}
451 	}
452 
453 	return true;
454 }
455 
456 /* https://dom.spec.whatwg.org/#dom-domtokenlist-add */
PHP_METHOD(Dom_TokenList,add)457 PHP_METHOD(Dom_TokenList, add)
458 {
459 	zval *args;
460 	uint32_t argc;
461 	ZEND_PARSE_PARAMETERS_START(0, -1)
462 		Z_PARAM_VARIADIC('*', args, argc)
463 	ZEND_PARSE_PARAMETERS_END();
464 
465 	/* 1. For each token in tokens (...) */
466 	if (!dom_validate_tokens_varargs(args, argc)) {
467 		RETURN_THROWS();
468 	}
469 
470 	/* 2. For each token in tokens, append token to this’s token set. */
471 	dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
472 	dom_token_list_ensure_set_up_to_date(intern);
473 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
474 	for (uint32_t i = 0; i < argc; i++) {
475 		dom_add_token(token_set, Z_STR(args[i]));
476 	}
477 
478 	/* 3. Run the update steps. */
479 	dom_token_list_update(intern);
480 }
481 
482 /* https://dom.spec.whatwg.org/#dom-domtokenlist-remove */
PHP_METHOD(Dom_TokenList,remove)483 PHP_METHOD(Dom_TokenList, remove)
484 {
485 	zval *args;
486 	uint32_t argc;
487 	ZEND_PARSE_PARAMETERS_START(0, -1)
488 		Z_PARAM_VARIADIC('*', args, argc)
489 	ZEND_PARSE_PARAMETERS_END();
490 
491 	/* 1. For each token in tokens (...) */
492 	if (!dom_validate_tokens_varargs(args, argc)) {
493 		RETURN_THROWS();
494 	}
495 
496 	/* 2. For each token in tokens, remove token from this’s token set. */
497 	dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
498 	dom_token_list_ensure_set_up_to_date(intern);
499 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
500 	for (uint32_t i = 0; i < argc; i++) {
501 		zend_hash_del(token_set, Z_STR(args[i]));
502 	}
503 
504 	/* 3. Run the update steps. */
505 	dom_token_list_update(intern);
506 }
507 
508 /* https://dom.spec.whatwg.org/#dom-domtokenlist-toggle */
PHP_METHOD(Dom_TokenList,toggle)509 PHP_METHOD(Dom_TokenList, toggle)
510 {
511 	zend_string *token;
512 	bool force, force_not_given = true;
513 	ZEND_PARSE_PARAMETERS_START(1, 2)
514 		Z_PARAM_PATH_STR(token)
515 		Z_PARAM_OPTIONAL
516 		Z_PARAM_BOOL_OR_NULL(force, force_not_given)
517 	ZEND_PARSE_PARAMETERS_END();
518 
519 	/* Steps 1 - 2 */
520 	if (!dom_validate_token(token)) {
521 		RETURN_THROWS();
522 	}
523 
524 	/* 3. If this’s token set[token] exists, then: */
525 	dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
526 	dom_token_list_ensure_set_up_to_date(intern);
527 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
528 	zval *found_token = zend_hash_find(token_set, token);
529 	if (found_token != NULL) {
530 		ZEND_ASSERT(XtOffsetOf(Bucket, val) == 0 && "the cast only works if this is true");
531 		Bucket *bucket = (Bucket *) found_token;
532 
533 		/* 3.1. If force is either not given or is false, then remove token from this’s token set,
534 		 *      run the update steps and return false. */
535 		if (force_not_given || !force) {
536 			zend_hash_del_bucket(token_set, bucket);
537 			dom_token_list_update(intern);
538 			RETURN_FALSE;
539 		}
540 
541 		/* 3.2. Return true. */
542 		RETURN_TRUE;
543 	}
544 	/* 4. Otherwise, if force not given or is true, append token to this’s token set,
545 	 *    run the update steps, and return true. */
546 	else if (force_not_given || force) {
547 		dom_add_token(token_set, token);
548 		dom_token_list_update(intern);
549 		RETURN_TRUE;
550 	}
551 
552 	/* 5. Return false. */
553 	RETURN_FALSE;
554 }
555 
556 /* https://dom.spec.whatwg.org/#dom-domtokenlist-replace */
PHP_METHOD(Dom_TokenList,replace)557 PHP_METHOD(Dom_TokenList, replace)
558 {
559 	zend_string *token, *new_token;
560 	ZEND_PARSE_PARAMETERS_START(2, 2)
561 		Z_PARAM_PATH_STR(token)
562 		Z_PARAM_PATH_STR(new_token)
563 	ZEND_PARSE_PARAMETERS_END();
564 
565 	/* Steps 1 - 2 */
566 	if (!dom_validate_token(token) || !dom_validate_token(new_token)) {
567 		RETURN_THROWS();
568 	}
569 
570 	/* 3. If this’s token set does not contain token, then return false. */
571 	dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
572 	dom_token_list_ensure_set_up_to_date(intern);
573 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
574 	zval *found_token = zend_hash_find(token_set, token);
575 	if (found_token == NULL) {
576 		RETURN_FALSE;
577 	}
578 
579 	/* 4. Replace token in this’s token set with newToken. */
580 	ZEND_ASSERT(XtOffsetOf(Bucket, val) == 0 && "the cast only works if this is true");
581 	Bucket *bucket = (Bucket *) found_token;
582 	if (zend_hash_set_bucket_key(token_set, bucket, new_token) == NULL) {
583 		/* It already exists, remove token instead. */
584 		zend_hash_del_bucket(token_set, bucket);
585 	} else {
586 		Z_STR(bucket->val) = new_token;
587 	}
588 
589 	/* 5. Run the update steps. */
590 	dom_token_list_update(intern);
591 
592 	/* 6. Return true. */
593 	RETURN_TRUE;
594 }
595 
596 /* https://dom.spec.whatwg.org/#concept-domtokenlist-validation */
PHP_METHOD(Dom_TokenList,supports)597 PHP_METHOD(Dom_TokenList, supports)
598 {
599 	zend_string *token;
600 	ZEND_PARSE_PARAMETERS_START(1, 1)
601 		Z_PARAM_PATH_STR(token)
602 	ZEND_PARSE_PARAMETERS_END();
603 
604 	/* The spec designers have designed the TokenList API with future usages in mind.
605 	 * But right now, this should just always throw a TypeError because the only user is classList, which
606 	 * does not define a supported token set. */
607 	zend_throw_error(zend_ce_type_error, "Attribute \"class\" does not define any supported tokens");
608 }
609 
PHP_METHOD(Dom_TokenList,count)610 PHP_METHOD(Dom_TokenList, count)
611 {
612 	ZEND_PARSE_PARAMETERS_NONE();
613 	dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL();
614 	dom_token_list_ensure_set_up_to_date(intern);
615 	RETURN_LONG(zend_hash_num_elements(TOKEN_LIST_GET_SET(intern)));
616 }
617 
PHP_METHOD(Dom_TokenList,getIterator)618 PHP_METHOD(Dom_TokenList, getIterator)
619 {
620 	ZEND_PARSE_PARAMETERS_NONE();
621 	zend_create_internal_iterator_zval(return_value, ZEND_THIS);
622 }
623 
dom_token_list_it_dtor(zend_object_iterator * iter)624 static void dom_token_list_it_dtor(zend_object_iterator *iter)
625 {
626 	zval_ptr_dtor(&iter->data);
627 }
628 
dom_token_list_it_rewind(zend_object_iterator * iter)629 static void dom_token_list_it_rewind(zend_object_iterator *iter)
630 {
631 	dom_token_list_it     *iterator = (dom_token_list_it *) iter;
632 	dom_token_list_object *object   = Z_TOKEN_LIST_P(&iter->data);
633 	zend_hash_internal_pointer_reset_ex(TOKEN_LIST_GET_SET(object), &iterator->pos);
634 }
635 
dom_token_list_it_valid(zend_object_iterator * iter)636 static zend_result dom_token_list_it_valid(zend_object_iterator *iter)
637 {
638 	dom_token_list_it     *iterator  = (dom_token_list_it *) iter;
639 	dom_token_list_object *object    = Z_TOKEN_LIST_P(&iter->data);
640 	HashTable             *token_set = TOKEN_LIST_GET_SET(object);
641 
642 	dom_token_list_ensure_set_up_to_date(object);
643 
644 	iterator->pos = zend_hash_get_current_pos_ex(token_set, iterator->pos);
645 
646 	return iterator->pos >= token_set->nNumUsed ? FAILURE : SUCCESS;
647 }
648 
dom_token_list_it_get_current_data(zend_object_iterator * iter)649 static zval *dom_token_list_it_get_current_data(zend_object_iterator *iter)
650 {
651 	dom_token_list_it     *iterator  = (dom_token_list_it *) iter;
652 	dom_token_list_object *object    = Z_TOKEN_LIST_P(&iter->data);
653 	dom_token_list_ensure_set_up_to_date(object);
654 	/* Caller manages the refcount of the data. */
655 	return zend_hash_get_current_data_ex(TOKEN_LIST_GET_SET(object), &iterator->pos);
656 }
657 
dom_token_list_it_get_current_key(zend_object_iterator * iter,zval * key)658 static void dom_token_list_it_get_current_key(zend_object_iterator *iter, zval *key)
659 {
660 	dom_token_list_it	  *iterator  = (dom_token_list_it *) iter;
661 	dom_token_list_object *object    = Z_TOKEN_LIST_P(&iter->data);
662 
663 	dom_token_list_ensure_set_up_to_date(object);
664 
665 	if (UNEXPECTED(php_libxml_is_cache_tag_stale(&object->cache_tag, &iterator->cache_tag))) {
666 		iter->index = 0;
667 		HashPosition pos;
668 		HashTable *token_set = TOKEN_LIST_GET_SET(object);
669 		zend_hash_internal_pointer_reset_ex(token_set, &pos);
670 		while (pos != iterator->pos) {
671 			iter->index++;
672 			zend_hash_move_forward_ex(token_set, &pos);
673 		}
674 	}
675 
676 	ZVAL_LONG(key, iter->index);
677 }
678 
dom_token_list_it_move_forward(zend_object_iterator * iter)679 static void dom_token_list_it_move_forward(zend_object_iterator *iter)
680 {
681 	dom_token_list_it     *iterator  = (dom_token_list_it *) iter;
682 	dom_token_list_object *object    = Z_TOKEN_LIST_P(&iter->data);
683 	HashTable             *token_set = TOKEN_LIST_GET_SET(object);
684 
685 	dom_token_list_ensure_set_up_to_date(object);
686 
687 	HashPosition current = iterator->pos;
688 	HashPosition validated = zend_hash_get_current_pos_ex(token_set, iterator->pos);
689 
690 	/* Check if already moved due to user operations, if so don't move again but reset to the first valid position,
691 	 * otherwise move one forward. */
692 	if (validated != current) {
693 		iterator->pos = validated;
694 	} else {
695 		zend_hash_move_forward_ex(token_set, &iterator->pos);
696 	}
697 }
698 
699 static const zend_object_iterator_funcs dom_token_list_it_funcs = {
700 	dom_token_list_it_dtor,
701 	dom_token_list_it_valid,
702 	dom_token_list_it_get_current_data,
703 	dom_token_list_it_get_current_key,
704 	dom_token_list_it_move_forward,
705 	dom_token_list_it_rewind,
706 	NULL, /* invalidate_current */
707 	NULL, /* get_gc */
708 };
709 
dom_token_list_get_iterator(zend_class_entry * ce,zval * object,int by_ref)710 zend_object_iterator *dom_token_list_get_iterator(zend_class_entry *ce, zval *object, int by_ref)
711 {
712 	if (by_ref) {
713 		zend_throw_error(NULL, "An iterator cannot be used with foreach by reference");
714 		return NULL;
715 	}
716 
717 	dom_token_list_object *intern = Z_TOKEN_LIST_P(object);
718 	dom_token_list_ensure_set_up_to_date(intern);
719 	HashTable *token_set = TOKEN_LIST_GET_SET(intern);
720 
721 	dom_token_list_it *iterator = emalloc(sizeof(*iterator));
722 	zend_iterator_init(&iterator->it);
723 	zend_hash_internal_pointer_reset_ex(token_set, &iterator->pos);
724 	ZVAL_OBJ_COPY(&iterator->it.data, Z_OBJ_P(object));
725 
726 	iterator->it.funcs = &dom_token_list_it_funcs;
727 	iterator->cache_tag = intern->cache_tag;
728 
729 	return &iterator->it;
730 }
731 
732 #endif
733