/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Authors: Christian Stocker <chregu@php.net>                          |
   |          Rob Richards <rrichards@php.net>                            |
   |          Nora Dossche  <ndossche@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 "internal_helpers.h"
#include <libxml/parserInternals.h>

static void xpath_callbacks_entry_dtor(zval *zv)
{
	zend_fcall_info_cache *fcc = Z_PTR_P(zv);
	zend_fcc_dtor(fcc);
	efree(fcc);
}

PHP_DOM_EXPORT void php_dom_xpath_callback_ns_ctor(php_dom_xpath_callback_ns *ns)
{
	zend_hash_init(&ns->functions, 0, NULL, xpath_callbacks_entry_dtor, false);
	ns->mode = PHP_DOM_REG_FUNC_MODE_NONE;
}

PHP_DOM_EXPORT void php_dom_xpath_callback_ns_dtor(php_dom_xpath_callback_ns *ns)
{
	zend_hash_destroy(&ns->functions);
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_ctor(php_dom_xpath_callbacks *registry)
{
	registry->php_ns = NULL;
	registry->namespaces = NULL;
	registry->node_list = NULL;
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry)
{
	if (registry->node_list) {
		zend_hash_destroy(registry->node_list);
		FREE_HASHTABLE(registry->node_list);
		registry->node_list = NULL;
	}
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args)
{
	for (uint32_t i = 0; i < num_args; i++) {
		xmlXPathObjectPtr obj = valuePop(ctxt);
		xmlXPathFreeObject(obj);
	}

	/* Don't push a sentinel value here. If this is called from an error situation, then by *not* pushing a sentinel
	 * the execution will halt. If this is called from a regular situation, then it is the caller's responsibility
	 * to ensure the stack remains balanced. */
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry)
{
	if (registry->php_ns) {
		php_dom_xpath_callback_ns_dtor(registry->php_ns);
		efree(registry->php_ns);
	}
	if (registry->namespaces) {
		php_dom_xpath_callback_ns *ns;
		ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
			php_dom_xpath_callback_ns_dtor(ns);
			efree(ns);
		} ZEND_HASH_FOREACH_END();

		zend_hash_destroy(registry->namespaces);
		FREE_HASHTABLE(registry->namespaces);
	}
	php_dom_xpath_callbacks_clean_node_list(registry);
}

static void php_dom_xpath_callback_ns_get_gc(php_dom_xpath_callback_ns *ns, zend_get_gc_buffer *gc_buffer)
{
	zend_fcall_info_cache *entry;
	ZEND_HASH_MAP_FOREACH_PTR(&ns->functions, entry) {
		zend_get_gc_buffer_add_fcc(gc_buffer, entry);
	} ZEND_HASH_FOREACH_END();
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer)
{
	if (registry->php_ns) {
		php_dom_xpath_callback_ns_get_gc(registry->php_ns, gc_buffer);
	}
	if (registry->namespaces) {
		php_dom_xpath_callback_ns *ns;
		ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
			php_dom_xpath_callback_ns_get_gc(ns, gc_buffer);
		} ZEND_HASH_FOREACH_END();
	}
}

PHP_DOM_EXPORT HashTable *php_dom_xpath_callbacks_get_gc_for_whole_object(php_dom_xpath_callbacks *registry, zend_object *object, zval **table, int *n)
{
	if (registry->php_ns || registry->namespaces) {
		zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
		php_dom_xpath_callbacks_get_gc(registry, gc_buffer);
		zend_get_gc_buffer_use(gc_buffer, table, n);

		if (object->properties == NULL && object->ce->default_properties_count == 0) {
			return NULL;
		} else {
			return zend_std_get_properties(object);
		}
	} else {
		return zend_std_get_gc(object, table, n);
	}
}

static bool php_dom_xpath_is_callback_name_valid(const zend_string *name, php_dom_xpath_callback_name_validation name_validation)
{
	if (ZSTR_LEN(name) == 0) {
		return false;
	}

	if (name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS || name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME) {
		if (zend_str_has_nul_byte(name)) {
			return false;
		}
	}

	if (name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME) {
		if (xmlValidateNCName(BAD_CAST ZSTR_VAL(name), /* pass 0 to disallow spaces */ 0) != 0) {
			return false;
		}
	}

	return true;
}

static bool php_dom_xpath_is_callback_name_valid_and_throw(const zend_string *name, php_dom_xpath_callback_name_validation name_validation, bool is_array)
{
	if (!php_dom_xpath_is_callback_name_valid(name, name_validation)) {
		if (is_array) {
			zend_argument_value_error(1, "must be an array containing valid callback names");
		} else {
			zend_argument_value_error(2, "must be a valid callback name");
		}
		return false;
	}
	return true;
}

PHP_DOM_EXPORT void php_dom_xpath_callbacks_delayed_lib_registration(const php_dom_xpath_callbacks* registry, void *ctxt, php_dom_xpath_callbacks_register_func_ctx register_func)
{
	if (registry->namespaces) {
		zend_string *namespace;
		php_dom_xpath_callback_ns *ns;
		ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(registry->namespaces, namespace, ns) {
			zend_string *name;
			ZEND_HASH_MAP_FOREACH_STR_KEY(&ns->functions, name) {
				register_func(ctxt, namespace, name);
			} ZEND_HASH_FOREACH_END();
		} ZEND_HASH_FOREACH_END();
	}
}

static zend_result php_dom_xpath_callback_ns_update_method_handler(
	php_dom_xpath_callback_ns* ns,
	xmlXPathContextPtr ctxt,
	const zend_string *namespace,
	zend_string *name,
	const HashTable *callable_ht,
	php_dom_xpath_callback_name_validation name_validation,
	php_dom_xpath_callbacks_register_func_ctx register_func
)
{
	zval *entry, registered_value;

	if (callable_ht) {
		zend_string *key;
		ZEND_HASH_FOREACH_STR_KEY_VAL(callable_ht, key, entry) {
			zend_fcall_info_cache* fcc = emalloc(sizeof(*fcc));
			char *error;
			if (!zend_is_callable_ex(entry, NULL, 0, NULL, fcc, &error)) {
				zend_argument_type_error(1, "must be an array with valid callbacks as values, %s", error);
				efree(fcc);
				efree(error);
				return FAILURE;
			}

			zend_fcc_addref(fcc);
			ZVAL_PTR(&registered_value, fcc);

			if (!key) {
				zend_string *tmp_str;
				zend_string *str = zval_try_get_tmp_string(entry, &tmp_str);
				if (str && php_dom_xpath_is_callback_name_valid_and_throw(str, name_validation, true)) {
					zend_hash_update(&ns->functions, str, &registered_value);
					if (register_func) {
						register_func(ctxt, namespace, str);
					}
					zend_tmp_string_release(tmp_str);
				} else {
					zend_tmp_string_release(tmp_str);
					zend_fcc_dtor(fcc);
					efree(fcc);
					return FAILURE;
				}
			} else {
				if (!php_dom_xpath_is_callback_name_valid_and_throw(key, name_validation, true)) {
					zend_fcc_dtor(fcc);
					efree(fcc);
					return FAILURE;
				}
				zend_hash_update(&ns->functions, key, &registered_value);
				if (register_func) {
					register_func(ctxt, namespace, key);
				}
			}
		} ZEND_HASH_FOREACH_END();
		ns->mode = PHP_DOM_REG_FUNC_MODE_SET;
	} else if (name) {
		if (!php_dom_xpath_is_callback_name_valid(name, name_validation)) {
			zend_argument_value_error(1, "must be a valid callback name");
			return FAILURE;
		}
		zend_fcall_info_cache* fcc = emalloc(sizeof(*fcc));
		char *error;
		zval tmp;
		ZVAL_STR(&tmp, name);
		if (!zend_is_callable_ex(&tmp, NULL, 0, NULL, fcc, &error)) {
			zend_argument_type_error(1, "must be a callable, %s", error);
			efree(fcc);
			efree(error);
			return FAILURE;
		}
		zend_fcc_addref(fcc);
		ZVAL_PTR(&registered_value, fcc);
		zend_hash_update(&ns->functions, name, &registered_value);
		if (register_func) {
			register_func(ctxt, namespace, name);
		}
		ns->mode = PHP_DOM_REG_FUNC_MODE_SET;
	} else {
		ns->mode = PHP_DOM_REG_FUNC_MODE_ALL;
	}

	return SUCCESS;
}

static php_dom_xpath_callback_ns *php_dom_xpath_callbacks_ensure_ns(php_dom_xpath_callbacks *registry, zend_string *ns)
{
	if (ns == NULL) {
		if (!registry->php_ns) {
			registry->php_ns = emalloc(sizeof(*registry->php_ns));
			php_dom_xpath_callback_ns_ctor(registry->php_ns);
		}
		return registry->php_ns;
	} else {
		if (!registry->namespaces) {
			/* In most cases probably only a single namespace is registered. */
			registry->namespaces = zend_new_array(1);
		}
		php_dom_xpath_callback_ns *namespace = zend_hash_find_ptr(registry->namespaces, ns);
		if (namespace == NULL) {
			namespace = emalloc(sizeof(*namespace));
			php_dom_xpath_callback_ns_ctor(namespace);
			zend_hash_add_new_ptr(registry->namespaces, ns, namespace);
		}
		return namespace;
	}
}

PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, xmlXPathContextPtr ctxt, zend_string *ns, zend_string *name, const HashTable *callable_ht, php_dom_xpath_callback_name_validation name_validation, php_dom_xpath_callbacks_register_func_ctx register_func)
{
	php_dom_xpath_callback_ns *namespace = php_dom_xpath_callbacks_ensure_ns(registry, ns);
	return php_dom_xpath_callback_ns_update_method_handler(namespace, ctxt, ns, name, callable_ht, name_validation, register_func);
}

PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_single_method_handler(php_dom_xpath_callbacks* registry, xmlXPathContextPtr ctxt, zend_string *ns, zend_string *name, const zend_fcall_info_cache *fcc, php_dom_xpath_callback_name_validation name_validation, php_dom_xpath_callbacks_register_func_ctx register_func)
{
	if (!php_dom_xpath_is_callback_name_valid_and_throw(name, name_validation, false)) {
		return FAILURE;
	}

	php_dom_xpath_callback_ns *namespace = php_dom_xpath_callbacks_ensure_ns(registry, ns);
	zend_fcall_info_cache* allocated_fcc = emalloc(sizeof(*allocated_fcc));
	zend_fcc_dup(allocated_fcc, fcc);

	zval registered_value;
	ZVAL_PTR(&registered_value, allocated_fcc);

	zend_hash_update(&namespace->functions, name, &registered_value);
	if (register_func) {
		register_func(ctxt, ns, name);
	}

	namespace->mode = PHP_DOM_REG_FUNC_MODE_SET;

	return SUCCESS;
}

static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, uint32_t param_count, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory)
{
	if (param_count == 0) {
		return NULL;
	}

	zval *params = safe_emalloc(param_count, sizeof(zval), 0);

	/* Reverse order to pop values off ctxt stack */
	for (zval *param = params + param_count - 1; param >= params; param--) {
		xmlXPathObjectPtr obj = valuePop(ctxt);
		ZEND_ASSERT(obj != NULL);
		switch (obj->type) {
			case XPATH_STRING:
				ZVAL_STRING(param, (char *)obj->stringval);
				break;
			case XPATH_BOOLEAN:
				ZVAL_BOOL(param, obj->boolval);
				break;
			case XPATH_NUMBER:
				ZVAL_DOUBLE(param, obj->floatval);
				break;
			case XPATH_NODESET:
				if (evaluation_mode == PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING) {
					char *str = (char *)xmlXPathCastToString(obj);
					ZVAL_STRING(param, str);
					xmlFree(str);
				} else if (evaluation_mode == PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET) {
					if (obj->nodesetval && obj->nodesetval->nodeNr > 0) {
						array_init_size(param, obj->nodesetval->nodeNr);
						zend_hash_real_init_packed(Z_ARRVAL_P(param));
						for (int j = 0; j < obj->nodesetval->nodeNr; j++) {
							xmlNodePtr node = obj->nodesetval->nodeTab[j];
							zval child;
							if (UNEXPECTED(node->type == XML_NAMESPACE_DECL)) {
								xmlNodePtr nsparent = node->_private;
								xmlNsPtr original = (xmlNsPtr) node;

								/* Make sure parent dom object exists, so we can take an extra reference. */
								zval parent_zval; /* don't destroy me, my lifetime is transferred to the fake namespace decl */
								php_dom_create_object(nsparent, &parent_zval, intern);
								dom_object *parent_intern = Z_DOMOBJ_P(&parent_zval);

								php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
							} else {
								proxy_factory(node, &child, intern, ctxt);
							}
							zend_hash_next_index_insert_new(Z_ARRVAL_P(param), &child);
						}
					} else {
						ZVAL_EMPTY_ARRAY(param);
					}
				}
				break;
			default: {
				char *str = (char *)xmlXPathCastToString(obj);
				ZVAL_STRING(param, str);
				xmlFree(str);
				break;
			}
		}
		xmlXPathFreeObject(obj);
	}

	return params;
}

static void php_dom_xpath_callback_cleanup_args(php_dom_xpath_callbacks *xpath_callbacks, zval *params, uint32_t param_count)
{
	if (params) {
		for (uint32_t i = 0; i < param_count; i++) {
			zval *param = &params[i];
			if (Z_TYPE_P(param) == IS_OBJECT || Z_TYPE_P(param) == IS_ARRAY) {
				if (xpath_callbacks->node_list == NULL) {
					xpath_callbacks->node_list = zend_new_array(0);
				}
				zend_hash_next_index_insert_new(xpath_callbacks->node_list, param);
			} else {
				zval_ptr_dtor(param);
			}
		}
		efree(params);
	}
}

static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpath_callbacks, php_dom_xpath_callback_ns *ns, xmlXPathParserContextPtr ctxt, zval *params, uint32_t param_count, const char *function_name, size_t function_name_length)
{
    zval callback_retval;

	if (UNEXPECTED(ns == NULL)) {
		zend_throw_error(NULL, "No callbacks were registered");
		return FAILURE;
	}

	if (ns->mode == PHP_DOM_REG_FUNC_MODE_ALL) {
		zend_fcall_info fci;
		fci.size = sizeof(fci);
		fci.object = NULL;
		fci.retval = &callback_retval;
		fci.param_count = param_count;
		fci.params = params;
		fci.named_params = NULL;
		fci.consumed_args = 0;
		ZVAL_STRINGL(&fci.function_name, function_name, function_name_length);

		zend_call_function(&fci, NULL);
		zend_string_release_ex(Z_STR(fci.function_name), false);
		if (UNEXPECTED(EG(exception))) {
			return FAILURE;
		}
	} else {
		ZEND_ASSERT(ns->mode == PHP_DOM_REG_FUNC_MODE_SET);

		zval *fcc_wrapper = zend_hash_str_find(&ns->functions, function_name, function_name_length);
		if (fcc_wrapper) {
			zend_call_known_fcc(Z_PTR_P(fcc_wrapper), &callback_retval, param_count, params, NULL);
		} else {
			zend_throw_error(NULL, "No callback handler \"%s\" registered", function_name);
			return FAILURE;
		}
	}

	if (Z_TYPE(callback_retval) != IS_UNDEF) {
		if (Z_TYPE(callback_retval) == IS_OBJECT
		 && (instanceof_function(Z_OBJCE(callback_retval), dom_get_node_ce(php_dom_follow_spec_node((const xmlNode *) ctxt->context->doc))))) {
			if (xpath_callbacks->node_list == NULL) {
				xpath_callbacks->node_list = zend_new_array(0);
			}
			zend_hash_next_index_insert_new(xpath_callbacks->node_list, &callback_retval);
			dom_object *obj = Z_DOMOBJ_P(&callback_retval);
			xmlNodePtr nodep = dom_object_get_node(obj);
			valuePush(ctxt, xmlXPathNewNodeSet(nodep));
		} else if (Z_TYPE(callback_retval) == IS_FALSE || Z_TYPE(callback_retval) == IS_TRUE) {
			valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(callback_retval) == IS_TRUE));
		} else if (Z_TYPE(callback_retval) == IS_OBJECT) {
			zend_type_error("Only objects that are instances of DOM nodes can be converted to an XPath expression");
			zval_ptr_dtor(&callback_retval);
			return FAILURE;
		} else {
			convert_to_string(&callback_retval);
			valuePush(ctxt, xmlXPathNewString(BAD_CAST Z_STRVAL(callback_retval)));
			zval_ptr_dtor_str(&callback_retval);
		}
	}

	return SUCCESS;
}

PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, int num_args, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory)
{
	zend_result result = FAILURE;

	if (UNEXPECTED(num_args == 0)) {
		zend_throw_error(NULL, "Function name must be passed as the first argument");
		goto cleanup_no_obj;
	}

	uint32_t param_count = num_args - 1;
	zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory);

	/* Last element of the stack is the function name */
	xmlXPathObjectPtr obj = valuePop(ctxt);
	if (UNEXPECTED(obj->stringval == NULL)) {
		zend_type_error("Handler name must be a string");
		goto cleanup;
	}

	const char *function_name = (const char *) obj->stringval;
	size_t function_name_length = strlen(function_name);

	result = php_dom_xpath_callback_dispatch(xpath_callbacks, xpath_callbacks->php_ns, ctxt, params, param_count, function_name, function_name_length);

cleanup:
	xmlXPathFreeObject(obj);
	php_dom_xpath_callback_cleanup_args(xpath_callbacks, params, param_count);
cleanup_no_obj:
	if (UNEXPECTED(result != SUCCESS)) {
		/* Push sentinel value */
		valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
	}

	return result;
}

PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_custom_ns(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, int num_args, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory)
{
	uint32_t param_count = num_args;
	zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory);

	const char *namespace = (const char *) ctxt->context->functionURI;
	/* Impossible because it wouldn't have been registered inside the context. */
	ZEND_ASSERT(xpath_callbacks->namespaces != NULL);

	php_dom_xpath_callback_ns *ns = zend_hash_str_find_ptr(xpath_callbacks->namespaces, namespace, strlen(namespace));
	/* Impossible because it wouldn't have been registered inside the context. */
	ZEND_ASSERT(ns != NULL);

	const char *function_name = (const char *) ctxt->context->function;
	size_t function_name_length = strlen(function_name);

	zend_result result = php_dom_xpath_callback_dispatch(xpath_callbacks, ns, ctxt, params, param_count, function_name, function_name_length);

	php_dom_xpath_callback_cleanup_args(xpath_callbacks, params, param_count);
	if (UNEXPECTED(result != SUCCESS)) {
		/* Push sentinel value */
		valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
	}

	return result;
}

#endif
