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