php-src/ext/dom/parentnode/css_selectors.c
2024-08-30 01:20:56 +02:00

282 lines
8.3 KiB
C

/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| https://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Niels Dossche <nielsdos@php.net> |
+----------------------------------------------------------------------+
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "../php_dom.h"
#include "lexbor/css/parser.h"
#include "lexbor/selectors-adapted/selectors.h"
// TODO: optimization idea: cache the parsed selectors in an LRU fashion?
typedef struct {
HashTable *list;
dom_object *intern;
} dom_query_selector_all_ctx;
typedef struct {
const xmlNode *reference;
bool result;
} dom_query_selector_matches_ctx;
static lxb_selectors_opt_t dom_quirks_opt(lxb_selectors_opt_t options, const dom_object *intern)
{
if (intern->document != NULL && intern->document->quirks_mode) {
options |= LXB_SELECTORS_OPT_QUIRKS_MODE;
}
return options;
}
lxb_status_t dom_query_selector_find_single_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
xmlNodePtr *result = (xmlNodePtr *) ctx;
*result = (xmlNodePtr) node;
return LXB_STATUS_STOP;
}
lxb_status_t dom_query_selector_find_array_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
dom_query_selector_all_ctx *qsa_ctx = (dom_query_selector_all_ctx *) ctx;
zval object;
php_dom_create_object((xmlNodePtr) node, &object, qsa_ctx->intern);
zend_hash_next_index_insert_new(qsa_ctx->list, &object);
return LXB_STATUS_OK;
}
lxb_status_t dom_query_selector_find_matches_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
dom_query_selector_matches_ctx *matches_ctx = (dom_query_selector_matches_ctx *) ctx;
if (node == matches_ctx->reference) {
matches_ctx->result = true;
return LXB_STATUS_STOP;
}
return LXB_STATUS_OK;
}
static lxb_css_selector_list_t *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
)
{
lxb_status_t status;
memset(parser, 0, sizeof(lxb_css_parser_t));
status = lxb_css_parser_init(parser, NULL);
ZEND_ASSERT(status == LXB_STATUS_OK);
memset(selectors, 0, sizeof(lxb_selectors_t));
status = lxb_selectors_init(selectors);
ZEND_ASSERT(status == LXB_STATUS_OK);
lxb_selectors_opt_set(selectors, dom_quirks_opt(options, intern));
lxb_css_selector_list_t *list = lxb_css_selectors_parse(parser, (const lxb_char_t *) ZSTR_VAL(selectors_str), ZSTR_LEN(selectors_str));
if (UNEXPECTED(list == NULL)) {
size_t nr_of_messages = lexbor_array_obj_length(&parser->log->messages);
if (nr_of_messages > 0) {
lxb_css_log_message_t *msg = lexbor_array_obj_get(&parser->log->messages, 0);
char *error;
zend_spprintf(&error, 0, "Invalid selector (%.*s)", (int) msg->text.length, msg->text.data);
php_dom_throw_error_with_message(SYNTAX_ERR, error, true);
efree(error);
} else {
php_dom_throw_error_with_message(SYNTAX_ERR, "Invalid selector", true);
}
}
return list;
}
static lxb_status_t dom_check_css_execution_status(lxb_status_t status)
{
if (UNEXPECTED(status != LXB_STATUS_OK && status != LXB_STATUS_STOP)) {
zend_argument_value_error(1, "contains an unsupported selector");
return status;
}
return LXB_STATUS_OK;
}
static void dom_selector_cleanup(lxb_css_parser_t *parser, lxb_selectors_t *selectors, lxb_css_selector_list_t *list)
{
lxb_css_selector_list_destroy_memory(list);
lxb_selectors_destroy(selectors);
(void) lxb_css_parser_destroy(parser, false);
}
static lxb_status_t 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
)
{
lxb_status_t status;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, options, intern);
if (UNEXPECTED(list == NULL)) {
status = LXB_STATUS_ERROR;
} else {
status = lxb_selectors_find(&selectors, root, list, cb, ctx);
status = dom_check_css_execution_status(status);
}
dom_selector_cleanup(&parser, &selectors, list);
return status;
}
static lxb_status_t dom_query_matches(
const xmlNode *root,
const dom_object *intern,
const zend_string *selectors_str,
void *ctx
)
{
lxb_status_t status;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
if (UNEXPECTED(list == NULL)) {
status = LXB_STATUS_ERROR;
} else {
status = lxb_selectors_match_node(&selectors, root, list, dom_query_selector_find_matches_callback, ctx);
status = dom_check_css_execution_status(status);
}
dom_selector_cleanup(&parser, &selectors, list);
return status;
}
static const xmlNode *dom_query_closest(
const xmlNode *root,
const dom_object *intern,
const zend_string *selectors_str
)
{
const xmlNode *ret = NULL;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
if (EXPECTED(list != NULL)) {
const xmlNode *current = root;
while (current != NULL) {
dom_query_selector_matches_ctx ctx = { current, false };
lxb_status_t status = lxb_selectors_match_node(&selectors, current, list, dom_query_selector_find_matches_callback, &ctx);
status = dom_check_css_execution_status(status);
if (UNEXPECTED(status != LXB_STATUS_OK)) {
break;
}
if (ctx.result) {
ret = current;
break;
}
current = current->parent;
}
}
dom_selector_cleanup(&parser, &selectors, list);
return ret;
}
/* https://dom.spec.whatwg.org/#dom-parentnode-queryselector */
void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
xmlNodePtr result = NULL;
if (dom_query_selector_common(
thisp,
intern,
selectors_str,
dom_query_selector_find_single_callback,
&result,
LXB_SELECTORS_OPT_MATCH_FIRST
) != LXB_STATUS_OK || result == NULL) {
RETURN_NULL();
} else {
DOM_RET_OBJ(result, intern);
}
}
/* https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall */
void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
HashTable *list = zend_new_array(0);
dom_query_selector_all_ctx ctx = { list, intern };
if (dom_query_selector_common(
thisp,
intern,
selectors_str,
dom_query_selector_find_array_callback,
&ctx,
LXB_SELECTORS_OPT_DEFAULT
) != LXB_STATUS_OK) {
zend_array_destroy(list);
RETURN_THROWS();
} else {
php_dom_create_iterator(return_value, DOM_NODELIST, true);
dom_object *ret_obj = Z_DOMOBJ_P(return_value);
dom_nnodemap_object *mapptr = (dom_nnodemap_object *) ret_obj->ptr;
ZVAL_ARR(&mapptr->baseobj_zv, list);
mapptr->nodetype = DOM_NODESET;
}
}
/* https://dom.spec.whatwg.org/#dom-element-matches */
void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
dom_query_selector_matches_ctx ctx = { thisp, false };
if (dom_query_matches(
thisp,
intern,
selectors_str,
&ctx
) != LXB_STATUS_OK) {
RETURN_THROWS();
} else {
RETURN_BOOL(ctx.result);
}
}
/* https://dom.spec.whatwg.org/#dom-element-closest */
void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
const xmlNode *result = dom_query_closest(thisp, intern, selectors_str);
if (EXPECTED(result != NULL)) {
DOM_RET_OBJ((xmlNodePtr) result, intern);
}
}
#endif