xref: /PHP-8.4/ext/dom/parentnode/css_selectors.c (revision 7ebdd7d0)
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 
25 #include "lexbor/css/parser.h"
26 #include "lexbor/selectors-adapted/selectors.h"
27 
28 // TODO: optimization idea: cache the parsed selectors in an LRU fashion?
29 
30 typedef struct {
31 	HashTable *list;
32 	dom_object *intern;
33 } dom_query_selector_all_ctx;
34 
35 typedef struct {
36 	const xmlNode *reference;
37 	bool result;
38 } dom_query_selector_matches_ctx;
39 
dom_quirks_opt(lxb_selectors_opt_t options,const dom_object * intern)40 static lxb_selectors_opt_t dom_quirks_opt(lxb_selectors_opt_t options, const dom_object *intern)
41 {
42 	if (intern->document != NULL && intern->document->quirks_mode) {
43 		options |= LXB_SELECTORS_OPT_QUIRKS_MODE;
44 	}
45 	return options;
46 }
47 
dom_query_selector_find_single_callback(const xmlNode * node,lxb_css_selector_specificity_t spec,void * ctx)48 lxb_status_t dom_query_selector_find_single_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
49 {
50 	xmlNodePtr *result = (xmlNodePtr *) ctx;
51 	*result = (xmlNodePtr) node;
52 	return LXB_STATUS_STOP;
53 }
54 
dom_query_selector_find_array_callback(const xmlNode * node,lxb_css_selector_specificity_t spec,void * ctx)55 lxb_status_t dom_query_selector_find_array_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
56 {
57 	dom_query_selector_all_ctx *qsa_ctx = (dom_query_selector_all_ctx *) ctx;
58 	zval object;
59 	php_dom_create_object((xmlNodePtr) node, &object, qsa_ctx->intern);
60 	zend_hash_next_index_insert_new(qsa_ctx->list, &object);
61 	return LXB_STATUS_OK;
62 }
63 
dom_query_selector_find_matches_callback(const xmlNode * node,lxb_css_selector_specificity_t spec,void * ctx)64 lxb_status_t dom_query_selector_find_matches_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
65 {
66 	dom_query_selector_matches_ctx *matches_ctx = (dom_query_selector_matches_ctx *) ctx;
67 	if (node == matches_ctx->reference) {
68 		matches_ctx->result = true;
69 		return LXB_STATUS_STOP;
70 	}
71 	return LXB_STATUS_OK;
72 }
73 
dom_parse_selector(lxb_css_parser_t * parser,lxb_selectors_t * selectors,const zend_string * selectors_str,lxb_selectors_opt_t options,const dom_object * intern)74 static lxb_css_selector_list_t *dom_parse_selector(
75 	lxb_css_parser_t *parser,
76 	lxb_selectors_t *selectors,
77 	const zend_string *selectors_str,
78 	lxb_selectors_opt_t options,
79 	const dom_object *intern
80 )
81 {
82 	lxb_status_t status;
83 
84 	memset(parser, 0, sizeof(lxb_css_parser_t));
85 	status = lxb_css_parser_init(parser, NULL);
86 	ZEND_ASSERT(status == LXB_STATUS_OK);
87 
88 	memset(selectors, 0, sizeof(lxb_selectors_t));
89 	status = lxb_selectors_init(selectors);
90 	ZEND_ASSERT(status == LXB_STATUS_OK);
91 	lxb_selectors_opt_set(selectors, dom_quirks_opt(options, intern));
92 
93 	lxb_css_selector_list_t *list = lxb_css_selectors_parse(parser, (const lxb_char_t *) ZSTR_VAL(selectors_str), ZSTR_LEN(selectors_str));
94 	if (UNEXPECTED(list == NULL)) {
95 		size_t nr_of_messages = lexbor_array_obj_length(&parser->log->messages);
96 		if (nr_of_messages > 0) {
97 			lxb_css_log_message_t *msg = lexbor_array_obj_get(&parser->log->messages, 0);
98 			char *error;
99 			zend_spprintf(&error, 0, "Invalid selector (%.*s)", (int) msg->text.length, msg->text.data);
100 			php_dom_throw_error_with_message(SYNTAX_ERR, error, true);
101 			efree(error);
102 		} else {
103 			php_dom_throw_error_with_message(SYNTAX_ERR, "Invalid selector", true);
104 		}
105 	}
106 
107 	return list;
108 }
109 
dom_check_css_execution_status(lxb_status_t status)110 static lxb_status_t dom_check_css_execution_status(lxb_status_t status)
111 {
112 	if (UNEXPECTED(status != LXB_STATUS_OK && status != LXB_STATUS_STOP)) {
113 		zend_argument_value_error(1, "contains an unsupported selector");
114 		return status;
115 	}
116 	return LXB_STATUS_OK;
117 }
118 
dom_selector_cleanup(lxb_css_parser_t * parser,lxb_selectors_t * selectors,lxb_css_selector_list_t * list)119 static void dom_selector_cleanup(lxb_css_parser_t *parser, lxb_selectors_t *selectors, lxb_css_selector_list_t *list)
120 {
121 	lxb_css_selector_list_destroy_memory(list);
122 	lxb_selectors_destroy(selectors);
123 	(void) lxb_css_parser_destroy(parser, false);
124 }
125 
dom_query_selector_common(const xmlNode * root,const dom_object * intern,const zend_string * selectors_str,lxb_selectors_cb_f cb,void * ctx,lxb_selectors_opt_t options)126 static lxb_status_t dom_query_selector_common(
127 	const xmlNode *root,
128 	const dom_object *intern,
129 	const zend_string *selectors_str,
130 	lxb_selectors_cb_f cb,
131 	void *ctx,
132 	lxb_selectors_opt_t options
133 )
134 {
135 	lxb_status_t status;
136 
137 	lxb_css_parser_t parser;
138 	lxb_selectors_t selectors;
139 
140 	lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, options, intern);
141 	if (UNEXPECTED(list == NULL)) {
142 		status = LXB_STATUS_ERROR;
143 	} else {
144 		status = lxb_selectors_find(&selectors, root, list, cb, ctx);
145 		status = dom_check_css_execution_status(status);
146 	}
147 
148 	dom_selector_cleanup(&parser, &selectors, list);
149 
150 	return status;
151 }
152 
dom_query_matches(const xmlNode * root,const dom_object * intern,const zend_string * selectors_str,void * ctx)153 static lxb_status_t dom_query_matches(
154 	const xmlNode *root,
155 	const dom_object *intern,
156 	const zend_string *selectors_str,
157 	void *ctx
158 )
159 {
160 	lxb_status_t status;
161 
162 	lxb_css_parser_t parser;
163 	lxb_selectors_t selectors;
164 
165 	lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
166 	if (UNEXPECTED(list == NULL)) {
167 		status = LXB_STATUS_ERROR;
168 	} else {
169 		status = lxb_selectors_match_node(&selectors, root, list, dom_query_selector_find_matches_callback, ctx);
170 		status = dom_check_css_execution_status(status);
171 	}
172 
173 	dom_selector_cleanup(&parser, &selectors, list);
174 
175 	return status;
176 }
177 
dom_query_closest(const xmlNode * root,const dom_object * intern,const zend_string * selectors_str)178 static const xmlNode *dom_query_closest(
179 	const xmlNode *root,
180 	const dom_object *intern,
181 	const zend_string *selectors_str
182 )
183 {
184 	const xmlNode *ret = NULL;
185 
186 	lxb_css_parser_t parser;
187 	lxb_selectors_t selectors;
188 
189 	lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
190 	if (EXPECTED(list != NULL)) {
191 		const xmlNode *current = root;
192 		while (current != NULL) {
193 			dom_query_selector_matches_ctx ctx = { current, false };
194 			lxb_status_t status = lxb_selectors_match_node(&selectors, current, list, dom_query_selector_find_matches_callback, &ctx);
195 			status = dom_check_css_execution_status(status);
196 			if (UNEXPECTED(status != LXB_STATUS_OK)) {
197 				break;
198 			}
199 			if (ctx.result) {
200 				ret = current;
201 				break;
202 			}
203 			current = current->parent;
204 		}
205 	}
206 
207 	dom_selector_cleanup(&parser, &selectors, list);
208 
209 	return ret;
210 }
211 
212 /* https://dom.spec.whatwg.org/#dom-parentnode-queryselector */
dom_parent_node_query_selector(xmlNodePtr thisp,dom_object * intern,zval * return_value,const zend_string * selectors_str)213 void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
214 {
215 	xmlNodePtr result = NULL;
216 
217 	if (dom_query_selector_common(
218 		thisp,
219 		intern,
220 		selectors_str,
221 		dom_query_selector_find_single_callback,
222 		&result,
223 		LXB_SELECTORS_OPT_MATCH_FIRST
224 	) != LXB_STATUS_OK || result == NULL) {
225 		RETURN_NULL();
226 	} else {
227 		DOM_RET_OBJ(result, intern);
228 	}
229 }
230 
231 /* https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall */
dom_parent_node_query_selector_all(xmlNodePtr thisp,dom_object * intern,zval * return_value,const zend_string * selectors_str)232 void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
233 {
234 	HashTable *list = zend_new_array(0);
235 	dom_query_selector_all_ctx ctx = { list, intern };
236 
237 	if (dom_query_selector_common(
238 		thisp,
239 		intern,
240 		selectors_str,
241 		dom_query_selector_find_array_callback,
242 		&ctx,
243 		LXB_SELECTORS_OPT_DEFAULT
244 	) != LXB_STATUS_OK) {
245 		zend_array_destroy(list);
246 		RETURN_THROWS();
247 	} else {
248 		php_dom_create_iterator(return_value, DOM_NODELIST, true);
249 		dom_object *ret_obj = Z_DOMOBJ_P(return_value);
250 		dom_nnodemap_object *mapptr = (dom_nnodemap_object *) ret_obj->ptr;
251 		ZVAL_ARR(&mapptr->baseobj_zv, list);
252 		mapptr->nodetype = DOM_NODESET;
253 	}
254 }
255 
256 /* https://dom.spec.whatwg.org/#dom-element-matches */
dom_element_matches(xmlNodePtr thisp,dom_object * intern,zval * return_value,const zend_string * selectors_str)257 void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
258 {
259 	dom_query_selector_matches_ctx ctx = { thisp, false };
260 
261 	if (dom_query_matches(
262 		thisp,
263 		intern,
264 		selectors_str,
265 		&ctx
266 	) != LXB_STATUS_OK) {
267 		RETURN_THROWS();
268 	} else {
269 		RETURN_BOOL(ctx.result);
270 	}
271 }
272 
273 /* https://dom.spec.whatwg.org/#dom-element-closest */
dom_element_closest(xmlNodePtr thisp,dom_object * intern,zval * return_value,const zend_string * selectors_str)274 void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
275 {
276 	const xmlNode *result = dom_query_closest(thisp, intern, selectors_str);
277 	if (EXPECTED(result != NULL)) {
278 		DOM_RET_OBJ((xmlNodePtr) result, intern);
279 	}
280 }
281 
282 #endif
283