From 84d143f21a8a2c4c0d2976d0a2d64835d93a97a5 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 2 Nov 2023 20:59:36 +0100
Subject: [PATCH 01/12] Improve callbacks in ext/dom and ext/xsl
---
ext/dom/config.m4 | 4 +-
ext/dom/config.w32 | 4 +-
ext/dom/php_dom.c | 33 +-
ext/dom/php_dom.h | 5 +-
ext/dom/tests/DOMXPath_callables.phpt | 127 +++++++
ext/dom/tests/domxpath.phpt | 4 +-
ext/dom/xpath.c | 213 +++---------
ext/dom/xpath_callbacks.c | 315 ++++++++++++++++++
ext/dom/xpath_callbacks.h | 55 +++
ext/xsl/php_xsl.c | 19 +-
ext/xsl/php_xsl.h | 13 +-
ext/xsl/tests/XSLTProcessor_callables.phpt | 150 +++++++++
ext/xsl/tests/php_function_edge_cases.phpt | 19 +-
ext/xsl/tests/throw_in_autoload.phpt | 4 +-
ext/xsl/tests/xslt011.phpt | 59 ++--
ext/xsl/tests/xslt011.xsl | 2 -
ext/xsl/tests/xslt_non_dom_node.phpt | 31 ++
ext/xsl/tests/xslt_non_dom_node.xsl | 9 +
...registerPHPFunctions-array-notallowed.phpt | 12 +-
...sor_registerPHPFunctions-funcnostring.phpt | 12 +-
...cessor_registerPHPFunctions-funcundef.phpt | 12 +-
...egisterPHPFunctions-string-notallowed.phpt | 12 +-
ext/xsl/xsltprocessor.c | 264 ++-------------
23 files changed, 860 insertions(+), 518 deletions(-)
create mode 100644 ext/dom/tests/DOMXPath_callables.phpt
create mode 100644 ext/dom/xpath_callbacks.c
create mode 100644 ext/dom/xpath_callbacks.h
create mode 100644 ext/xsl/tests/XSLTProcessor_callables.phpt
create mode 100644 ext/xsl/tests/xslt_non_dom_node.phpt
create mode 100644 ext/xsl/tests/xslt_non_dom_node.xsl
diff --git a/ext/dom/config.m4 b/ext/dom/config.m4
index 041ebff7eb918..8a7d5daac61fa 100644
--- a/ext/dom/config.m4
+++ b/ext/dom/config.m4
@@ -35,7 +35,7 @@ if test "$PHP_DOM" != "no"; then
nodelist.c text.c comment.c \
entityreference.c \
notation.c xpath.c dom_iterators.c \
- namednodemap.c \
+ namednodemap.c xpath_callbacks.c \
$LEXBOR_SOURCES],
$ext_shared,,$PHP_LEXBOR_CFLAGS)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/ports/posix/lexbor/core)
@@ -49,7 +49,7 @@ if test "$PHP_DOM" != "no"; then
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/ns)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/tag)
PHP_SUBST(DOM_SHARED_LIBADD)
- PHP_INSTALL_HEADERS([ext/dom/xml_common.h])
+ PHP_INSTALL_HEADERS([ext/dom/xml_common.h ext/dom/xpath_callbacks.h])
PHP_ADD_EXTENSION_DEP(dom, libxml)
])
fi
diff --git a/ext/dom/config.w32 b/ext/dom/config.w32
index a18e8ebe3a60f..bb0101b960b2f 100644
--- a/ext/dom/config.w32
+++ b/ext/dom/config.w32
@@ -15,7 +15,7 @@ if (PHP_DOM == "yes") {
entity.c nodelist.c text.c comment.c \
entityreference.c \
notation.c xpath.c dom_iterators.c \
- namednodemap.c", null, "-Iext/dom/lexbor");
+ namednodemap.c xpath_callbacks.c", null, "-Iext/dom/lexbor");
ADD_SOURCES("ext/dom/lexbor/lexbor/ports/windows_nt/lexbor/core", "memory.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/core", "array_obj.c array.c avl.c bst.c diyfp.c conv.c dobject.c dtoa.c hash.c mem.c mraw.c print.c serialize.c shs.c str.c strtod.c", "dom");
@@ -41,7 +41,7 @@ if (PHP_DOM == "yes") {
WARNING("dom support can't be enabled, libxml is not found")
}
}
- PHP_INSTALL_HEADERS("ext/dom", "xml_common.h");
+ PHP_INSTALL_HEADERS("ext/dom", "xml_common.h xpath_callbacks.h");
} else {
WARNING("dom support can't be enabled, libxml is not enabled")
PHP_DOM = "no"
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 632da61c59cd2..c257367283788 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -596,8 +596,10 @@ static int dom_nodelist_has_dimension(zend_object *object, zval *member, int che
static zval *dom_nodemap_read_dimension(zend_object *object, zval *offset, int type, zval *rv);
static int dom_nodemap_has_dimension(zend_object *object, zval *member, int check_empty);
static zend_object *dom_objects_store_clone_obj(zend_object *zobject);
+
#ifdef LIBXML_XPATH_ENABLED
void dom_xpath_objects_free_storage(zend_object *object);
+HashTable *dom_xpath_get_gc(zend_object *object, zval **table, int *n);
#endif
static void *dom_malloc(size_t size) {
@@ -889,6 +891,7 @@ PHP_MINIT_FUNCTION(dom)
memcpy(&dom_xpath_object_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
dom_xpath_object_handlers.offset = XtOffsetOf(dom_xpath_object, dom) + XtOffsetOf(dom_object, std);
dom_xpath_object_handlers.free_obj = dom_xpath_objects_free_storage;
+ dom_xpath_object_handlers.get_gc = dom_xpath_get_gc;
dom_xpath_class_entry = register_class_DOMXPath();
dom_xpath_class_entry->create_object = dom_xpath_objects_new;
@@ -1001,32 +1004,6 @@ void node_list_unlink(xmlNodePtr node)
}
/* }}} end node_list_unlink */
-#ifdef LIBXML_XPATH_ENABLED
-/* {{{ dom_xpath_objects_free_storage */
-void dom_xpath_objects_free_storage(zend_object *object)
-{
- dom_xpath_object *intern = php_xpath_obj_from_obj(object);
-
- zend_object_std_dtor(&intern->dom.std);
-
- if (intern->dom.ptr != NULL) {
- xmlXPathFreeContext((xmlXPathContextPtr) intern->dom.ptr);
- php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
- }
-
- if (intern->registered_phpfunctions) {
- zend_hash_destroy(intern->registered_phpfunctions);
- FREE_HASHTABLE(intern->registered_phpfunctions);
- }
-
- if (intern->node_list) {
- zend_hash_destroy(intern->node_list);
- FREE_HASHTABLE(intern->node_list);
- }
-}
-/* }}} */
-#endif
-
/* {{{ dom_objects_free_storage */
void dom_objects_free_storage(zend_object *object)
{
@@ -1133,12 +1110,13 @@ static void dom_object_namespace_node_free_storage(zend_object *object)
}
#ifdef LIBXML_XPATH_ENABLED
+
/* {{{ zend_object dom_xpath_objects_new(zend_class_entry *class_type) */
zend_object *dom_xpath_objects_new(zend_class_entry *class_type)
{
dom_xpath_object *intern = zend_object_alloc(sizeof(dom_xpath_object), class_type);
- intern->registered_phpfunctions = zend_new_array(0);
+ php_dom_xpath_callbacks_ctor(&intern->xpath_callbacks);
intern->register_node_ns = 1;
intern->dom.prop_handler = &dom_xpath_prop_handlers;
@@ -1149,6 +1127,7 @@ zend_object *dom_xpath_objects_new(zend_class_entry *class_type)
return &intern->dom.std;
}
/* }}} */
+
#endif
void dom_nnodemap_objects_free_storage(zend_object *object) /* {{{ */
diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h
index bb778e7ab9d41..860e0bd697afc 100644
--- a/ext/dom/php_dom.h
+++ b/ext/dom/php_dom.h
@@ -53,6 +53,7 @@ extern zend_module_entry dom_module_entry;
#include "xml_common.h"
#include "ext/libxml/php_libxml.h"
+#include "xpath_callbacks.h"
#include "zend_exceptions.h"
#include "dom_ce.h"
/* DOM API_VERSION, please bump it up, if you change anything in the API
@@ -64,10 +65,8 @@ extern zend_module_entry dom_module_entry;
#define DOM_NODESET XML_XINCLUDE_START
typedef struct _dom_xpath_object {
- int registerPhpFunctions;
+ php_dom_xpath_callbacks xpath_callbacks;
int register_node_ns;
- HashTable *registered_phpfunctions;
- HashTable *node_list;
dom_object dom;
} dom_xpath_object;
diff --git a/ext/dom/tests/DOMXPath_callables.phpt b/ext/dom/tests/DOMXPath_callables.phpt
new file mode 100644
index 0000000000000..44068577520b0
--- /dev/null
+++ b/ext/dom/tests/DOMXPath_callables.phpt
@@ -0,0 +1,127 @@
+--TEST--
+registerPHPFunctions() with callables
+--EXTENSIONS--
+dom
+--FILE--
+registerPhpFunctions(["cycle" => array($this, "dummy")]);
+ }
+
+ public function dummy(string $var) {
+ echo "dummy: $var\n";
+ }
+}
+
+$doc = new DOMDocument();
+$doc->loadHTML('hello');
+
+echo "--- Legit cases: none ---\n";
+
+$xpath = new DOMXPath($doc);
+$xpath->registerNamespace("php", "http://php.net/xpath");
+try {
+ $xpath->evaluate("//a[php:function('var_dump', string(@href))]");
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: all ---\n";
+
+$xpath->registerPHPFunctions(null);
+$xpath->evaluate("//a[php:function('var_dump', string(@href))]");
+$xpath->evaluate("//a[php:function('MyClass::dump', string(@href))]");
+
+echo "--- Legit cases: set ---\n";
+
+$xpath = new DOMXPath($doc);
+$xpath->registerNamespace("php", "http://php.net/xpath");
+$xpath->registerPhpFunctions([]);
+$xpath->registerPHPFunctions(["xyz" => MyClass::dump(...), "mydump" => function (string $x) {
+ var_dump($x);
+}]);
+$xpath->registerPhpFunctions(str_repeat("var_dump", mt_rand(1, 1) /* defeat SCCP */));
+$xpath->evaluate("//a[php:function('mydump', string(@href))]");
+$xpath->evaluate("//a[php:function('xyz', string(@href))]");
+$xpath->evaluate("//a[php:function('var_dump', string(@href))]");
+try {
+ $xpath->evaluate("//a[php:function('notinset', string(@href))]");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: set with cycle ---\n";
+
+$xpath = new MyDOMXPath($doc);
+$xpath->registerNamespace("php", "http://php.net/xpath");
+$xpath->registerCycle();
+$xpath->evaluate("//a[php:function('cycle', string(@href))]");
+
+echo "--- Error cases ---\n";
+
+$xpath = new DOMXPath($doc);
+try {
+ $xpath->registerPhpFunctions("nonexistent");
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(function () {});
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions([function () {}]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions([var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(["nonexistent"]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(["" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+--- Legit cases: none ---
+No callbacks were registered
+--- Legit cases: all ---
+string(15) "https://php.net"
+string(15) "https://php.net"
+--- Legit cases: set ---
+string(15) "https://php.net"
+string(15) "https://php.net"
+string(15) "https://php.net"
+No callback handler "notinset" registered
+--- Legit cases: set with cycle ---
+dummy: https://php.net
+--- Error cases ---
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a callable, function "nonexistent" not found or invalid function name
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|string|null, Closure given
+Object of class Closure could not be converted to string
+Object of class Closure could not be converted to string
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) array key must not be empty
diff --git a/ext/dom/tests/domxpath.phpt b/ext/dom/tests/domxpath.phpt
index 79c82cccc685e..cead1f8d5b7c6 100644
--- a/ext/dom/tests/domxpath.phpt
+++ b/ext/dom/tests/domxpath.phpt
@@ -69,5 +69,5 @@ myval
float(1)
bool(true)
float(4)
-Unable to call handler non_existent()
-Unable to call handler non_existent()
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a callable, function "non_existent" not found or invalid function name
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "non_existant" not found or invalid function name
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index 7522ec3f1df86..dddd944882cdc 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -32,176 +32,74 @@
#ifdef LIBXML_XPATH_ENABLED
-static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, int type) /* {{{ */
+void dom_xpath_objects_free_storage(zend_object *object)
{
- zval retval;
- int result, i;
- int error = 0;
- zend_fcall_info fci;
- xmlXPathObjectPtr obj;
- char *str;
- zend_string *callable = NULL;
- dom_xpath_object *intern;
+ dom_xpath_object *intern = php_xpath_obj_from_obj(object);
+
+ zend_object_std_dtor(&intern->dom.std);
+
+ if (intern->dom.ptr != NULL) {
+ xmlXPathFreeContext((xmlXPathContextPtr) intern->dom.ptr);
+ php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
+ }
+
+ php_dom_xpath_callbacks_dtor(&intern->xpath_callbacks);
+}
+
+HashTable *dom_xpath_get_gc(zend_object *object, zval **table, int *n)
+{
+ dom_xpath_object *intern = php_xpath_obj_from_obj(object);
+ return php_dom_xpath_callbacks_get_gc_for_whole_object(&intern->xpath_callbacks, object, table, n);
+}
+
+static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern, xmlXPathParserContextPtr ctxt)
+{
+ (void) ctxt;
+
+ ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);
+ php_dom_create_object(node, child, intern);
+}
+
+static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+{
+ bool error = false;
+ dom_xpath_object *intern;
if (! zend_is_executing()) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: Function called from outside of PHP\n");
- error = 1;
+ error = true;
} else {
intern = (dom_xpath_object *) ctxt->context->userData;
if (intern == NULL) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: failed to get the internal object\n");
- error = 1;
- }
- else if (intern->registerPhpFunctions == 0) {
- xmlGenericError(xmlGenericErrorContext,
- "xmlExtFunctionTest: PHP Object did not register PHP functions\n");
- error = 1;
- }
- }
-
- if (error == 1) {
- for (i = nargs - 1; i >= 0; i--) {
- obj = valuePop(ctxt);
- xmlXPathFreeObject(obj);
+ error = true;
}
- return;
- }
-
- if (UNEXPECTED(nargs == 0)) {
- zend_throw_error(NULL, "Function name must be passed as the first argument");
- return;
- }
-
- fci.param_count = nargs - 1;
- if (fci.param_count > 0) {
- fci.params = safe_emalloc(fci.param_count, sizeof(zval), 0);
- }
- /* Reverse order to pop values off ctxt stack */
- for (i = fci.param_count - 1; i >= 0; i--) {
- obj = valuePop(ctxt);
- switch (obj->type) {
- case XPATH_STRING:
- ZVAL_STRING(&fci.params[i], (char *)obj->stringval);
- break;
- case XPATH_BOOLEAN:
- ZVAL_BOOL(&fci.params[i], obj->boolval);
- break;
- case XPATH_NUMBER:
- ZVAL_DOUBLE(&fci.params[i], obj->floatval);
- break;
- case XPATH_NODESET:
- if (type == 1) {
- str = (char *)xmlXPathCastToString(obj);
- ZVAL_STRING(&fci.params[i], str);
- xmlFree(str);
- } else if (type == 2) {
- int j;
- if (obj->nodesetval && obj->nodesetval->nodeNr > 0) {
- array_init_size(&fci.params[i], obj->nodesetval->nodeNr);
- zend_hash_real_init_packed(Z_ARRVAL_P(&fci.params[i]));
- for (j = 0; j < obj->nodesetval->nodeNr; j++) {
- xmlNodePtr node = obj->nodesetval->nodeTab[j];
- zval child;
- if (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 transfered to the fake namespace decl */
- php_dom_create_object(nsparent, &parent_zval, &intern->dom);
- dom_object *parent_intern = Z_DOMOBJ_P(&parent_zval);
-
- node = php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
- } else {
- php_dom_create_object(node, &child, &intern->dom);
- }
- add_next_index_zval(&fci.params[i], &child);
- }
- } else {
- ZVAL_EMPTY_ARRAY(&fci.params[i]);
- }
- }
- break;
- default:
- ZVAL_STRING(&fci.params[i], (char *)xmlXPathCastToString(obj));
+ else if (intern->xpath_callbacks.mode == PHP_DOM_REG_FUNC_MODE_NONE) {
+ zend_throw_error(NULL, "No callbacks were registered");
+ error = true;
}
- xmlXPathFreeObject(obj);
}
- fci.size = sizeof(fci);
-
- /* Last element of the stack is the function name */
- obj = valuePop(ctxt);
- if (obj->stringval == NULL) {
- zend_type_error("Handler name must be a string");
- xmlXPathFreeObject(obj);
- goto cleanup_no_callable;
- }
- ZVAL_STRING(&fci.function_name, (char *) obj->stringval);
- xmlXPathFreeObject(obj);
-
- fci.object = NULL;
- fci.named_params = NULL;
- fci.retval = &retval;
-
- if (!zend_make_callable(&fci.function_name, &callable)) {
- zend_throw_error(NULL, "Unable to call handler %s()", ZSTR_VAL(callable));
- goto cleanup;
- } else if (intern->registerPhpFunctions == 2 && zend_hash_exists(intern->registered_phpfunctions, callable) == 0) {
- zend_throw_error(NULL, "Not allowed to call handler '%s()'.", ZSTR_VAL(callable));
- goto cleanup;
+ if (error) {
+ php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
- result = zend_call_function(&fci, NULL);
- if (result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
- if (Z_TYPE(retval) == IS_OBJECT && instanceof_function(Z_OBJCE(retval), dom_node_class_entry)) {
- xmlNode *nodep;
- dom_object *obj;
- if (intern->node_list == NULL) {
- intern->node_list = zend_new_array(0);
- }
- Z_ADDREF(retval);
- zend_hash_next_index_insert(intern->node_list, &retval);
- obj = Z_DOMOBJ_P(&retval);
- nodep = dom_object_get_node(obj);
- valuePush(ctxt, xmlXPathNewNodeSet(nodep));
- } else if (Z_TYPE(retval) == IS_FALSE || Z_TYPE(retval) == IS_TRUE) {
- valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(retval) == IS_TRUE));
- } else if (Z_TYPE(retval) == IS_OBJECT) {
- zend_type_error("A PHP Object cannot be converted to a XPath-string");
- return;
- } else {
- zend_string *str = zval_get_string(&retval);
- valuePush(ctxt, xmlXPathNewString((xmlChar *) ZSTR_VAL(str)));
- zend_string_release_ex(str, 0);
- }
- zval_ptr_dtor(&retval);
- }
- }
-cleanup:
- zend_string_release_ex(callable, 0);
- zval_ptr_dtor_nogc(&fci.function_name);
-cleanup_no_callable:
- if (fci.param_count > 0) {
- for (i = 0; i < nargs - 1; i++) {
- zval_ptr_dtor(&fci.params[i]);
- }
- efree(fci.params);
+ php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
}
}
/* }}} */
static void dom_xpath_ext_function_string_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
{
- dom_xpath_ext_function_php(ctxt, nargs, 1);
+ dom_xpath_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING);
}
/* }}} */
static void dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
{
- dom_xpath_ext_function_php(ctxt, nargs, 2);
+ dom_xpath_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET);
}
/* }}} */
@@ -482,33 +380,8 @@ PHP_METHOD(DOMXPath, evaluate)
/* {{{ */
PHP_METHOD(DOMXPath, registerPhpFunctions)
{
- zval *id = ZEND_THIS;
- dom_xpath_object *intern = Z_XPATHOBJ_P(id);
- zval *entry, new_string;
- zend_string *name = NULL;
- HashTable *ht = NULL;
-
- ZEND_PARSE_PARAMETERS_START(0, 1)
- Z_PARAM_OPTIONAL
- Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(ht, name)
- ZEND_PARSE_PARAMETERS_END();
-
- if (ht) {
- ZEND_HASH_FOREACH_VAL(ht, entry) {
- zend_string *str = zval_get_string(entry);
- ZVAL_LONG(&new_string, 1);
- zend_hash_update(intern->registered_phpfunctions, str, &new_string);
- zend_string_release_ex(str, 0);
- } ZEND_HASH_FOREACH_END();
- intern->registerPhpFunctions = 2;
- } else if (name) {
- ZVAL_LONG(&new_string, 1);
- zend_hash_update(intern->registered_phpfunctions, name, &new_string);
- intern->registerPhpFunctions = 2;
- } else {
- intern->registerPhpFunctions = 1;
- }
-
+ dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
+ php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} end dom_xpath_register_php_functions */
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
new file mode 100644
index 0000000000000..5ec9466f398d0
--- /dev/null
+++ b/ext/dom/xpath_callbacks.c
@@ -0,0 +1,315 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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: Christian Stocker |
+ | Rob Richards |
+ | Niels Dossche |
+ +----------------------------------------------------------------------+
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "php.h"
+#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
+
+#include "php_dom.h"
+#include
+
+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_callbacks_ctor(php_dom_xpath_callbacks *registry)
+{
+ ALLOC_HASHTABLE(registry->functions);
+ zend_hash_init(registry->functions, 0, NULL, xpath_callbacks_entry_dtor, false);
+}
+
+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);
+ }
+
+ /* Push sentinel value */
+ valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
+}
+
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry)
+{
+ if (registry->functions) {
+ zend_hash_destroy(registry->functions);
+ FREE_HASHTABLE(registry->functions);
+ }
+ php_dom_xpath_callbacks_clean_node_list(registry);
+}
+
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer)
+{
+ zval *entry;
+ ZEND_HASH_FOREACH_VAL(registry->functions, entry) {
+ ZEND_ASSERT(Z_TYPE_P(entry) == IS_PTR);
+ zend_get_gc_buffer_add_fcc(gc_buffer, Z_PTR_P(entry));
+ } 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->mode == PHP_DOM_REG_FUNC_MODE_SET) {
+ 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);
+ }
+}
+
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, INTERNAL_FUNCTION_PARAMETERS)
+{
+ zval *entry, registered_value;
+ zend_string *name = NULL;
+ HashTable *ht = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(ht, name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (ht) {
+ zend_string *key;
+ ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, entry) {
+ zend_fcall_info_cache* fcc = emalloc(sizeof(zend_fcall_info));
+ 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_THROWS();
+ }
+
+ zend_fcc_addref(fcc);
+ ZVAL_PTR(®istered_value, fcc);
+
+ if (!key) {
+ zend_string *str = zval_try_get_string(entry);
+ if (str) {
+ zend_hash_update(registry->functions, str, ®istered_value);
+ zend_string_release_ex(str, 0);
+ } else {
+ zend_fcc_dtor(fcc);
+ efree(fcc);
+ RETURN_THROWS();
+ }
+ } else {
+ if (ZSTR_LEN(key) == 0) {
+ zend_argument_value_error(1, "array key must not be empty");
+ zend_fcc_dtor(fcc);
+ efree(fcc);
+ RETURN_THROWS();
+ }
+ zend_hash_update(registry->functions, key, ®istered_value);
+ }
+ } ZEND_HASH_FOREACH_END();
+ registry->mode = PHP_DOM_REG_FUNC_MODE_SET;
+ } else if (name) {
+ zend_fcall_info_cache* fcc = emalloc(sizeof(zend_fcall_info));
+ 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_THROWS();
+ }
+ zend_fcc_addref(fcc);
+ ZVAL_PTR(®istered_value, fcc);
+ zend_hash_update(registry->functions, name, ®istered_value);
+ registry->mode = PHP_DOM_REG_FUNC_MODE_SET;
+ } else {
+ registry->mode = PHP_DOM_REG_FUNC_MODE_ALL;
+ }
+}
+
+PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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)
+{
+ zval callback_retval;
+ zval *params = NULL;
+ 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;
+ if (param_count > 0) {
+ 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 transfered 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);
+ }
+ add_next_index_zval(param, &child);
+ }
+ } else {
+ ZVAL_EMPTY_ARRAY(param);
+ }
+ }
+ break;
+ default:
+ ZVAL_STRING(param, (char *)xmlXPathCastToString(obj));
+ break;
+ }
+ xmlXPathFreeObject(obj);
+ }
+ }
+
+ /* Last element of the stack is the function name */
+ xmlXPathObjectPtr obj = valuePop(ctxt);
+ if (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);
+
+ if (xpath_callbacks->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;
+ ZVAL_STRINGL(&fci.function_name, function_name, function_name_length);
+
+ zend_result result = zend_call_function(&fci, NULL);
+ zend_string_release_ex(Z_STR(fci.function_name), false);
+ if (UNEXPECTED(result == FAILURE)) {
+ goto cleanup;
+ }
+ } else {
+ ZEND_ASSERT(xpath_callbacks->mode == PHP_DOM_REG_FUNC_MODE_SET);
+
+ zval *fcc_wrapper = zend_hash_str_find(xpath_callbacks->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);
+ goto cleanup;
+ }
+ }
+
+ if (Z_TYPE(callback_retval) != IS_UNDEF) {
+ if (Z_TYPE(callback_retval) == IS_OBJECT && instanceof_function(Z_OBJCE(callback_retval), dom_node_class_entry)) {
+ xmlNode *nodep;
+ dom_object *obj;
+ if (xpath_callbacks->node_list == NULL) {
+ xpath_callbacks->node_list = zend_new_array(0);
+ }
+ Z_ADDREF_P(&callback_retval);
+ zend_hash_next_index_insert(xpath_callbacks->node_list, &callback_retval);
+ obj = Z_DOMOBJ_P(&callback_retval);
+ 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 DOMNode can be converted to an XPath expression");
+ zval_ptr_dtor(&callback_retval);
+ goto cleanup;
+ } else {
+ zend_string *str = zval_get_string(&callback_retval);
+ valuePush(ctxt, xmlXPathNewString((xmlChar *) ZSTR_VAL(str)));
+ zend_string_release_ex(str, 0);
+ }
+ zval_ptr_dtor(&callback_retval);
+ }
+
+ result = SUCCESS;
+
+cleanup:
+ xmlXPathFreeObject(obj);
+cleanup_no_obj:
+ if (UNEXPECTED(result != SUCCESS)) {
+ /* Push sentinel value */
+ valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
+ }
+ if (params) {
+ for (uint32_t i = 0; i < param_count; i++) {
+ zval_ptr_dtor(¶ms[i]);
+ }
+ efree(params);
+ }
+
+ return result;
+}
+
+#endif
diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h
new file mode 100644
index 0000000000000..1bde91e4ae751
--- /dev/null
+++ b/ext/dom/xpath_callbacks.h
@@ -0,0 +1,55 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef PHP_DOM_XPATH_CALLBACKS_H
+#define PHP_DOM_XPATH_CALLBACKS_H
+
+#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
+
+#include
+#include "xml_common.h"
+
+typedef enum {
+ PHP_DOM_REG_FUNC_MODE_NONE = 0,
+ PHP_DOM_REG_FUNC_MODE_ALL,
+ PHP_DOM_REG_FUNC_MODE_SET,
+} php_dom_register_functions_mode;
+
+typedef enum {
+ PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING,
+ PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET,
+} php_dom_xpath_nodeset_evaluation_mode;
+
+typedef void (*php_dom_xpath_callbacks_proxy_factory)(xmlNodePtr node, zval *proxy, dom_object *intern, xmlXPathParserContextPtr ctxt);
+
+typedef struct {
+ php_dom_register_functions_mode mode;
+ HashTable *functions;
+ HashTable *node_list;
+} php_dom_xpath_callbacks;
+
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_ctor(php_dom_xpath_callbacks *registry);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer);
+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);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, INTERNAL_FUNCTION_PARAMETERS);
+PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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);
+
+#endif
+#endif
+
diff --git a/ext/xsl/php_xsl.c b/ext/xsl/php_xsl.c
index 93945002b17e1..b294401e9c5a9 100644
--- a/ext/xsl/php_xsl.c
+++ b/ext/xsl/php_xsl.c
@@ -53,6 +53,12 @@ zend_module_entry xsl_module_entry = {
ZEND_GET_MODULE(xsl)
#endif
+static HashTable *xsl_objects_get_gc(zend_object *object, zval **table, int *n)
+{
+ xsl_object *intern = php_xsl_fetch_object(object);
+ return php_dom_xpath_callbacks_get_gc_for_whole_object(&intern->xpath_callbacks, object, table, n);
+}
+
/* {{{ xsl_objects_free_storage */
void xsl_objects_free_storage(zend_object *object)
{
@@ -65,15 +71,7 @@ void xsl_objects_free_storage(zend_object *object)
FREE_HASHTABLE(intern->parameter);
}
- if (intern->registered_phpfunctions) {
- zend_hash_destroy(intern->registered_phpfunctions);
- FREE_HASHTABLE(intern->registered_phpfunctions);
- }
-
- if (intern->node_list) {
- zend_hash_destroy(intern->node_list);
- FREE_HASHTABLE(intern->node_list);
- }
+ php_dom_xpath_callbacks_dtor(&intern->xpath_callbacks);
if (intern->doc) {
php_libxml_decrement_doc_ref(intern->doc);
@@ -106,7 +104,7 @@ zend_object *xsl_objects_new(zend_class_entry *class_type)
zend_object_std_init(&intern->std, class_type);
object_properties_init(&intern->std, class_type);
intern->parameter = zend_new_array(0);
- intern->registered_phpfunctions = zend_new_array(0);
+ php_dom_xpath_callbacks_ctor(&intern->xpath_callbacks);
return &intern->std;
}
@@ -119,6 +117,7 @@ PHP_MINIT_FUNCTION(xsl)
xsl_object_handlers.offset = XtOffsetOf(xsl_object, std);
xsl_object_handlers.clone_obj = NULL;
xsl_object_handlers.free_obj = xsl_objects_free_storage;
+ xsl_object_handlers.get_gc = xsl_objects_get_gc;
xsl_xsltprocessor_class_entry = register_class_XSLTProcessor();
xsl_xsltprocessor_class_entry->create_object = xsl_objects_new;
diff --git a/ext/xsl/php_xsl.h b/ext/xsl/php_xsl.h
index ed8dc9874bb90..2d264b2976d16 100644
--- a/ext/xsl/php_xsl.h
+++ b/ext/xsl/php_xsl.h
@@ -38,6 +38,7 @@ extern zend_module_entry xsl_module_entry;
#endif
#include "../dom/xml_common.h"
+#include "../dom/xpath_callbacks.h"
#include
#include
@@ -53,18 +54,14 @@ extern zend_module_entry xsl_module_entry;
typedef struct _xsl_object {
void *ptr;
- HashTable *prop_handler;
- zval handle;
HashTable *parameter;
int hasKeys;
- int registerPhpFunctions;
- HashTable *registered_phpfunctions;
- HashTable *node_list;
+ int securityPrefsSet;
+ zend_long securityPrefs;
+ php_dom_xpath_callbacks xpath_callbacks;
php_libxml_node_object *doc;
char *profiling;
- zend_long securityPrefs;
- int securityPrefsSet;
- zend_object std;
+ zend_object std;
} xsl_object;
static inline xsl_object *php_xsl_fetch_object(zend_object *obj) {
diff --git a/ext/xsl/tests/XSLTProcessor_callables.phpt b/ext/xsl/tests/XSLTProcessor_callables.phpt
new file mode 100644
index 0000000000000..1603fd4436451
--- /dev/null
+++ b/ext/xsl/tests/XSLTProcessor_callables.phpt
@@ -0,0 +1,150 @@
+--TEST--
+registerPhpFunctions() with callables
+--EXTENSIONS--
+xsl
+--FILE--
+registerPhpFunctions(["cycle" => array($this, "dummy")]);
+ }
+
+ public function dummy(string $var) {
+ return "dummy: $var";
+ }
+}
+
+function createProcessor($inputs, $class = "XSLTProcessor") {
+ $xsl = new DomDocument();
+ $xsl->loadXML('
+
+ '
+ . implode('', array_map(fn($input) => '', $inputs)) .
+ '
+ ');
+
+ $proc = new $class();
+ $proc->importStylesheet($xsl);
+ return $proc;
+}
+
+$inputdom = new DomDocument();
+$inputdom->loadXML('hello');
+
+echo "--- Legit cases: none ---\n";
+
+$proc = createProcessor(["'var_dump', string(@href)"]);
+try {
+ $proc->transformToXml($inputdom);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: all ---\n";
+
+$proc = createProcessor(["'var_dump', string(@href)", "'MyClass::dump', string(@href)"]);
+$proc->registerPHPFunctions();
+var_dump($proc->transformToXml($inputdom));
+
+echo "--- Legit cases: set ---\n";
+
+$proc = createProcessor(["'mydump', string(@href)", "'xyz', string(@href)", "'var_dump', string(@href)"]);
+$proc->registerPhpFunctions([]);
+$proc->registerPHPFunctions(["xyz" => MyClass::dump(...), "mydump" => function (string $x) {
+ var_dump($x);
+}]);
+$proc->registerPhpFunctions(str_repeat("var_dump", mt_rand(1, 1) /* defeat SCCP */));
+var_dump($proc->transformToXml($inputdom));
+
+$proc = createProcessor(["'notinset', string(@href)"]);
+$proc->registerPhpFunctions([]);
+try {
+ var_dump($proc->transformToXml($inputdom));
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: set with cycle ---\n";
+
+$proc = createProcessor(["'cycle', string(@href)"], 'MyXSLTProcessor');
+$proc->registerCycle();
+var_dump($proc->transformToXml($inputdom));
+
+echo "--- Error cases ---\n";
+
+$proc = createProcessor([]);
+try {
+ $proc->registerPhpFunctions("nonexistent");
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(function () {});
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions([function () {}]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions([var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(["nonexistent"]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(["" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+--- Legit cases: none ---
+No callbacks were registered
+--- Legit cases: all ---
+string(15) "https://php.net"
+string(15) "https://php.net"
+string(44) "
+dump: https://php.net
+"
+--- Legit cases: set ---
+string(15) "https://php.net"
+string(15) "https://php.net"
+string(15) "https://php.net"
+string(44) "
+dump: https://php.net
+"
+No callback handler "notinset" registered
+--- Legit cases: set with cycle ---
+string(45) "
+dummy: https://php.net
+"
+--- Error cases ---
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a callable, function "nonexistent" not found or invalid function name
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be of type array|string|null, Closure given
+Object of class Closure could not be converted to string
+Object of class Closure could not be converted to string
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) array key must not be empty
diff --git a/ext/xsl/tests/php_function_edge_cases.phpt b/ext/xsl/tests/php_function_edge_cases.phpt
index 23a06b111bb50..a8e0eb098d162 100644
--- a/ext/xsl/tests/php_function_edge_cases.phpt
+++ b/ext/xsl/tests/php_function_edge_cases.phpt
@@ -22,12 +22,8 @@ function test($input) {
$proc = new XsltProcessor();
$proc->registerPhpFunctions();
- $xsl = $proc->importStylesheet($xsl);
- try {
- $proc->transformToDoc($inputdom);
- } catch (Exception $e) {
- echo $e->getMessage(), "\n";
- }
+ $proc->importStylesheet($xsl);
+ $proc->transformToDoc($inputdom);
}
try {
@@ -36,10 +32,13 @@ try {
echo $e->getMessage(), "\n";
}
-test("3");
+try {
+ test("3");
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
?>
---EXPECTF--
+--EXPECT--
Function name must be passed as the first argument
-
-Warning: XSLTProcessor::transformToDoc(): Handler name must be a string in %s on line %d
+Handler name must be a string
diff --git a/ext/xsl/tests/throw_in_autoload.phpt b/ext/xsl/tests/throw_in_autoload.phpt
index 90ab0098b8e40..2df7d3690bed3 100644
--- a/ext/xsl/tests/throw_in_autoload.phpt
+++ b/ext/xsl/tests/throw_in_autoload.phpt
@@ -28,12 +28,14 @@ $proc->registerPhpFunctions();
$xsl = $proc->importStylesheet($xsl);
try {
$newdom = $proc->transformToDoc($inputdom);
-} catch (Exception $e) {
+} catch (Error $e) {
echo $e->getMessage(), "\n";
+ echo $e->getPrevious()->getMessage(), "\n";
}
?>
===DONE===
--EXPECT--
string(4) "TeSt"
+Invalid callback TeSt::dateLang, class "TeSt" not found
Autoload exception
===DONE===
diff --git a/ext/xsl/tests/xslt011.phpt b/ext/xsl/tests/xslt011.phpt
index 50279492b7bd3..d7b42f0395ce6 100644
--- a/ext/xsl/tests/xslt011.phpt
+++ b/ext/xsl/tests/xslt011.phpt
@@ -5,51 +5,42 @@ xsl
--FILE--
load(__DIR__."/xslt011.xsl");
- $proc = new xsltprocessor;
- $xsl = $proc->importStylesheet($dom);
+$dom->load(__DIR__."/xslt011.xsl");
+$proc = new xsltprocessor;
+$xsl = $proc->importStylesheet($dom);
- $xml = new DomDocument();
- $xml->load(__DIR__."/xslt011.xml");
- $proc->registerPHPFunctions();
- print $proc->transformToXml($xml);
+$xml = new DomDocument();
+$xml->load(__DIR__."/xslt011.xml");
+$proc->registerPHPFunctions();
+print $proc->transformToXml($xml);
- function foobar($id, $secondArg = "" ) {
- if (is_array($id)) {
- return $id[0]->value . " - " . $secondArg;
+function foobar($id, $secondArg = "" ) {
+ if (is_array($id)) {
+ return $id[0]->value . " - " . $secondArg;
+ } else {
+ return $id . " - " . $secondArg;
+ }
+}
+function nodeSet($id = null) {
+ if ($id and is_array($id)) {
+ return $id[0];
} else {
- return $id . " - " . $secondArg;
+ $dom = new domdocument;
+ $dom->loadXML("this is from an external DomDocument");
+ return $dom->documentElement;
}
- }
- function nodeSet($id = null) {
- if ($id and is_array($id)) {
- return $id[0];
- } else {
- $dom = new domdocument;
- $dom->loadXML("this is from an external DomDocument");
- return $dom->documentElement;
- }
- }
- function nonDomNode() {
- return new foo();
- }
+}
- class aClass {
- static function aStaticFunction($id) {
- return $id;
- }
+class aClass {
+ static function aStaticFunction($id) {
+ return $id;
}
+}
?>
--EXPECTF--
Test 11: php:function Support
-
-Warning: XSLTProcessor::transformToXml(): A PHP Object cannot be converted to a XPath-string in %s on line 16
foobar - secondArg
foobar -
diff --git a/ext/xsl/tests/xslt011.xsl b/ext/xsl/tests/xslt011.xsl
index e1960e57d3beb..39330d5eede17 100644
--- a/ext/xsl/tests/xslt011.xsl
+++ b/ext/xsl/tests/xslt011.xsl
@@ -19,7 +19,5 @@
-
-
diff --git a/ext/xsl/tests/xslt_non_dom_node.phpt b/ext/xsl/tests/xslt_non_dom_node.phpt
new file mode 100644
index 0000000000000..9ead8a67fb581
--- /dev/null
+++ b/ext/xsl/tests/xslt_non_dom_node.phpt
@@ -0,0 +1,31 @@
+--TEST--
+php:function Support - non-DOMNode
+--EXTENSIONS--
+xsl
+--FILE--
+load(__DIR__."/xslt_non_dom_node.xsl");
+$proc = new xsltprocessor;
+$xsl = $proc->importStylesheet($dom);
+
+$xml = new DomDocument();
+$xml->load(__DIR__."/xslt011.xml");
+$proc->registerPHPFunctions();
+try {
+ $proc->transformToXml($xml);
+} catch (TypeError $e) {
+ echo $e->getMessage();
+}
+?>
+--EXPECT--
+Only objects that are instances of DOMNode can be converted to an XPath expression
diff --git a/ext/xsl/tests/xslt_non_dom_node.xsl b/ext/xsl/tests/xslt_non_dom_node.xsl
new file mode 100644
index 0000000000000..e88cb6efe0554
--- /dev/null
+++ b/ext/xsl/tests/xslt_non_dom_node.xsl
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-array-notallowed.phpt b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-array-notallowed.phpt
index 668fef2f56784..6c3761f004310 100644
--- a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-array-notallowed.phpt
+++ b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-array-notallowed.phpt
@@ -13,13 +13,15 @@ if(!$phpfuncxsl) {
}
$proc->importStylesheet($phpfuncxsl);
var_dump($proc->registerPHPFunctions(array()));
-var_dump($proc->transformToXml($dom));
+try {
+ var_dump($proc->transformToXml($dom));
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
?>
---EXPECTF--
-NULL
-
-Warning: XSLTProcessor::transformToXml(): Not allowed to call handler 'ucwords()' in %s on line %d
+--EXPECT--
NULL
+No callback handler "ucwords" registered
--CREDITS--
Christian Weiske, cweiske@php.net
PHP Testfest Berlin 2009-05-09
diff --git a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcnostring.phpt b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcnostring.phpt
index d00b207b7652f..5356ff39e87d0 100644
--- a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcnostring.phpt
+++ b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcnostring.phpt
@@ -16,13 +16,15 @@ if(!$phpfuncxsl) {
}
$proc->importStylesheet($phpfuncxsl);
var_dump($proc->registerPHPFunctions());
-var_dump($proc->transformToXml($dom));
+try {
+ var_dump($proc->transformToXml($dom));
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
?>
---EXPECTF--
-NULL
-
-Warning: XSLTProcessor::transformToXml(): Handler name must be a string in %s on line %d
+--EXPECT--
NULL
+Handler name must be a string
--CREDITS--
Christian Weiske, cweiske@php.net
PHP Testfest Berlin 2009-05-09
diff --git a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcundef.phpt b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcundef.phpt
index 933d3da82c003..42ebcbb6c0867 100644
--- a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcundef.phpt
+++ b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-funcundef.phpt
@@ -15,13 +15,15 @@ if(!$phpfuncxsl) {
}
$proc->importStylesheet($phpfuncxsl);
var_dump($proc->registerPHPFunctions());
-var_dump($proc->transformToXml($dom));
+try {
+ var_dump($proc->transformToXml($dom));
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
?>
---EXPECTF--
-NULL
-
-Warning: XSLTProcessor::transformToXml(): Unable to call handler undefinedfunc() in %s on line %d
+--EXPECT--
NULL
+Invalid callback undefinedfunc, function "undefinedfunc" not found or invalid function name
--CREDITS--
Christian Weiske, cweiske@php.net
PHP Testfest Berlin 2009-05-09
diff --git a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-string-notallowed.phpt b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-string-notallowed.phpt
index be661ee410d33..17323d769fe36 100644
--- a/ext/xsl/tests/xsltprocessor_registerPHPFunctions-string-notallowed.phpt
+++ b/ext/xsl/tests/xsltprocessor_registerPHPFunctions-string-notallowed.phpt
@@ -13,13 +13,15 @@ if(!$phpfuncxsl) {
}
$proc->importStylesheet($phpfuncxsl);
var_dump($proc->registerPHPFunctions('strpos'));
-var_dump($proc->transformToXml($dom));
+try {
+ var_dump($proc->transformToXml($dom));
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
?>
---EXPECTF--
-NULL
-
-Warning: XSLTProcessor::transformToXml(): Not allowed to call handler 'ucwords()' in %s on line %d
+--EXPECT--
NULL
+No callback handler "ucwords" registered
--CREDITS--
Christian Weiske, cweiske@php.net
PHP Testfest Berlin 2009-05-09
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index f8cf57777ec08..4ebb0514a7b51 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -45,228 +45,72 @@ static zend_result php_xsl_xslt_apply_params(xsltTransformContextPtr ctxt, HashT
return SUCCESS;
}
-static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, int type) /* {{{ */
+static void xsl_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern, xmlXPathParserContextPtr ctxt)
{
- xsltTransformContextPtr tctxt;
- zval *args = NULL;
- zval retval;
- int i;
- int error = 0;
- zend_fcall_info fci;
- zval handler;
- xmlXPathObjectPtr obj;
- char *str;
- xsl_object *intern;
- zend_string *callable = NULL;
+ ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);
+
+ /**
+ * Upon freeing libxslt's context, every document that is not the *main* document will be freed by libxslt.
+ * If a node of a document that is *not the main* document gets returned to userland, we'd free the node twice:
+ * first by the cleanup of the xslt context, and then by our own refcounting mechanism.
+ * To prevent this, we'll take a copy if the node is not from the main document.
+ * It is important that we do not copy the node unconditionally, because that means that:
+ * - modifications to the node will only modify the copy, and not the original
+ * - accesses to the parent, path, ... will not work
+ */
+ xsltTransformContextPtr transform_ctxt = (xsltTransformContextPtr) ctxt->context->extra;
+ if (node->doc != transform_ctxt->document->doc) {
+ node = xmlDocCopyNode(node, intern->document->ptr, 1);
+ }
+ php_dom_create_object(node, child, intern);
+}
+static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+{
+ bool error = false;
+ xsl_object *intern;
if (! zend_is_executing()) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: Function called from outside of PHP\n");
- error = 1;
+ error = true;
} else {
- tctxt = xsltXPathGetTransformContext(ctxt);
+ xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt);
if (tctxt == NULL) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: failed to get the transformation context\n");
- error = 1;
+ error = true;
} else {
intern = (xsl_object*)tctxt->_private;
if (intern == NULL) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: failed to get the internal object\n");
- error = 1;
- }
- else if (intern->registerPhpFunctions == 0) {
- xsltGenericError(xsltGenericErrorContext,
- "xsltExtFunctionTest: PHP Object did not register PHP functions\n");
- error = 1;
+ error = true;
}
- }
- }
-
- if (error == 1) {
- for (i = nargs - 1; i >= 0; i--) {
- obj = valuePop(ctxt);
- if (obj) {
- xmlXPathFreeObject(obj);
- }
- }
- return;
- }
-
- if (UNEXPECTED(nargs == 0)) {
- zend_throw_error(NULL, "Function name must be passed as the first argument");
- return;
- }
-
- fci.param_count = nargs - 1;
- if (fci.param_count > 0) {
- args = safe_emalloc(fci.param_count, sizeof(zval), 0);
- }
- /* Reverse order to pop values off ctxt stack */
- for (i = fci.param_count - 1; i >= 0; i--) {
- obj = valuePop(ctxt);
- if (obj == NULL) {
- ZVAL_NULL(&args[i]);
- continue;
- }
- switch (obj->type) {
- case XPATH_STRING:
- ZVAL_STRING(&args[i], (char *)obj->stringval);
- break;
- case XPATH_BOOLEAN:
- ZVAL_BOOL(&args[i], obj->boolval);
- break;
- case XPATH_NUMBER:
- ZVAL_DOUBLE(&args[i], obj->floatval);
- break;
- case XPATH_NODESET:
- if (type == 1) {
- str = (char*)xmlXPathCastToString(obj);
- ZVAL_STRING(&args[i], str);
- xmlFree(str);
- } else if (type == 2) {
- int j;
- dom_object *domintern = (dom_object *)intern->doc;
- if (obj->nodesetval && obj->nodesetval->nodeNr > 0) {
- array_init(&args[i]);
- for (j = 0; j < obj->nodesetval->nodeNr; j++) {
- xmlNodePtr node = obj->nodesetval->nodeTab[j];
- zval child;
- /* not sure, if we need this... it's copied from xpath.c */
- if (node->type == XML_NAMESPACE_DECL) {
- xmlNsPtr curns;
- xmlNodePtr nsparent;
-
- nsparent = node->_private;
- curns = xmlNewNs(NULL, node->name, NULL);
- if (node->children) {
- curns->prefix = xmlStrdup((xmlChar *)node->children);
- }
- if (node->children) {
- node = xmlNewDocNode(node->doc, NULL, (xmlChar *) node->children, node->name);
- } else {
- node = xmlNewDocNode(node->doc, NULL, (const xmlChar *) "xmlns", node->name);
- }
- node->type = XML_NAMESPACE_DECL;
- node->parent = nsparent;
- node->ns = curns;
- } else {
- /**
- * Upon freeing libxslt's context, every document which is not the *main* document will be freed by libxslt.
- * If a node of a document which is *not the main* document gets returned to userland, we'd free the node twice:
- * first by the cleanup of the xslt context, and then by our own refcounting mechanism.
- * To prevent this, we'll take a copy if the node is not from the main document.
- * It is important that we do not copy the node unconditionally, because that means that:
- * - modifications to the node will only modify the copy, and not the original
- * - accesses to the parent, path, ... will not work
- */
- xsltTransformContextPtr transform_ctxt = (xsltTransformContextPtr) ctxt->context->extra;
- if (node->doc != transform_ctxt->document->doc) {
- node = xmlDocCopyNode(node, domintern->document->ptr, 1);
- }
- }
-
- php_dom_create_object(node, &child, domintern);
- add_next_index_zval(&args[i], &child);
- }
- } else {
- ZVAL_EMPTY_ARRAY(&args[i]);
- }
- }
- break;
- default:
- str = (char *) xmlXPathCastToString(obj);
- ZVAL_STRING(&args[i], str);
- xmlFree(str);
- }
- xmlXPathFreeObject(obj);
- }
-
- fci.size = sizeof(fci);
- fci.named_params = NULL;
- if (fci.param_count > 0) {
- fci.params = args;
- } else {
- fci.params = NULL;
- }
-
- /* Last element of the stack is the function name */
- obj = valuePop(ctxt);
- if (obj == NULL || obj->stringval == NULL) {
- php_error_docref(NULL, E_WARNING, "Handler name must be a string");
- xmlXPathFreeObject(obj);
- valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
- if (fci.param_count > 0) {
- for (i = 0; i < nargs - 1; i++) {
- zval_ptr_dtor(&args[i]);
+ else if (intern->xpath_callbacks.mode == PHP_DOM_REG_FUNC_MODE_NONE) {
+ zend_throw_error(NULL, "No callbacks were registered");
+ error = true;
}
- efree(args);
}
- return;
}
- ZVAL_STRING(&handler, (char *) obj->stringval);
- xmlXPathFreeObject(obj);
- ZVAL_COPY_VALUE(&fci.function_name, &handler);
- fci.object = NULL;
- fci.retval = &retval;
- if (!zend_make_callable(&handler, &callable)) {
- if (!EG(exception)) {
- php_error_docref(NULL, E_WARNING, "Unable to call handler %s()", ZSTR_VAL(callable));
- }
- valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
- } else if ( intern->registerPhpFunctions == 2 && zend_hash_exists(intern->registered_phpfunctions, callable) == 0) {
- php_error_docref(NULL, E_WARNING, "Not allowed to call handler '%s()'", ZSTR_VAL(callable));
- /* Push an empty string, so that we at least have an xslt result... */
- valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
+ if (error) {
+ php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
- zend_call_function(&fci, NULL);
- if (Z_ISUNDEF(retval)) {
- /* Exception thrown, don't do anything further. */
- } else if (Z_TYPE(retval) == IS_OBJECT && instanceof_function(Z_OBJCE(retval), dom_node_class_entry)) {
- xmlNode *nodep;
- dom_object *obj;
- if (intern->node_list == NULL) {
- intern->node_list = zend_new_array(0);
- }
- Z_ADDREF(retval);
- zend_hash_next_index_insert(intern->node_list, &retval);
- obj = Z_DOMOBJ_P(&retval);
- nodep = dom_object_get_node(obj);
- valuePush(ctxt, xmlXPathNewNodeSet(nodep));
- } else if (Z_TYPE(retval) == IS_TRUE || Z_TYPE(retval) == IS_FALSE) {
- valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(retval) == IS_TRUE));
- } else if (Z_TYPE(retval) == IS_OBJECT) {
- php_error_docref(NULL, E_WARNING, "A PHP Object cannot be converted to a XPath-string");
- valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
- } else {
- convert_to_string(&retval);
- valuePush(ctxt, xmlXPathNewString((xmlChar *) Z_STRVAL(retval)));
- }
- zval_ptr_dtor(&retval);
- }
- zend_string_release_ex(callable, 0);
- zval_ptr_dtor_nogc(&handler);
- if (fci.param_count > 0) {
- for (i = 0; i < nargs - 1; i++) {
- zval_ptr_dtor(&args[i]);
- }
- efree(args);
+ php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, (dom_object *) intern->doc, xsl_proxy_factory);
}
}
/* }}} */
void xsl_ext_function_string_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
{
- xsl_ext_function_php(ctxt, nargs, 1);
+ xsl_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING);
}
/* }}} */
void xsl_ext_function_object_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
{
- xsl_ext_function_php(ctxt, nargs, 2);
+ xsl_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET);
}
/* }}} */
@@ -475,11 +319,7 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
xsltFreeSecurityPrefs(secPrefs);
}
- if (intern->node_list != NULL) {
- zend_hash_destroy(intern->node_list);
- FREE_HASHTABLE(intern->node_list);
- intern->node_list = NULL;
- }
+ php_dom_xpath_callbacks_clean_node_list(&intern->xpath_callbacks);
php_libxml_decrement_doc_ref(intern->doc);
efree(intern->doc);
@@ -734,38 +574,8 @@ PHP_METHOD(XSLTProcessor, removeParameter)
/* {{{ */
PHP_METHOD(XSLTProcessor, registerPHPFunctions)
{
- zval *id = ZEND_THIS;
- xsl_object *intern;
- zval *entry, new_string;
- zend_string *restrict_str = NULL;
- HashTable *restrict_ht = NULL;
-
- ZEND_PARSE_PARAMETERS_START(0, 1)
- Z_PARAM_OPTIONAL
- Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(restrict_ht, restrict_str)
- ZEND_PARSE_PARAMETERS_END();
-
- intern = Z_XSL_P(id);
-
- if (restrict_ht) {
- ZEND_HASH_FOREACH_VAL(restrict_ht, entry) {
- zend_string *str = zval_try_get_string(entry);
- if (UNEXPECTED(!str)) {
- return;
- }
- ZVAL_LONG(&new_string, 1);
- zend_hash_update(intern->registered_phpfunctions, str, &new_string);
- zend_string_release(str);
- } ZEND_HASH_FOREACH_END();
-
- intern->registerPhpFunctions = 2;
- } else if (restrict_str) {
- ZVAL_LONG(&new_string, 1);
- zend_hash_update(intern->registered_phpfunctions, restrict_str, &new_string);
- intern->registerPhpFunctions = 2;
- } else {
- intern->registerPhpFunctions = 1;
- }
+ xsl_object *intern = Z_XSL_P(ZEND_THIS);
+ php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} end XSLTProcessor::registerPHPFunctions(); */
From b0092841d8d52b4cfb246507066592077f6cbc6d Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Tue, 28 Nov 2023 22:23:40 +0100
Subject: [PATCH 02/12] Refactor code to allow multiple namespaces
---
ext/dom/tests/DOMXPath_callables.phpt | 7 +
ext/dom/xpath.c | 15 +-
ext/dom/xpath_callbacks.c | 284 ++++++++++++++++----------
ext/dom/xpath_callbacks.h | 9 +-
ext/xsl/xsltprocessor.c | 15 +-
5 files changed, 207 insertions(+), 123 deletions(-)
diff --git a/ext/dom/tests/DOMXPath_callables.phpt b/ext/dom/tests/DOMXPath_callables.phpt
index 44068577520b0..aa45de536222a 100644
--- a/ext/dom/tests/DOMXPath_callables.phpt
+++ b/ext/dom/tests/DOMXPath_callables.phpt
@@ -65,6 +65,11 @@ $xpath->registerNamespace("php", "http://php.net/xpath");
$xpath->registerCycle();
$xpath->evaluate("//a[php:function('cycle', string(@href))]");
+echo "--- Legit cases: reset to null ---\n";
+
+$xpath->registerPhpFunctions(null);
+$xpath->evaluate("//a[php:function('var_dump', string(@href))]");
+
echo "--- Error cases ---\n";
$xpath = new DOMXPath($doc);
@@ -118,6 +123,8 @@ string(15) "https://php.net"
No callback handler "notinset" registered
--- Legit cases: set with cycle ---
dummy: https://php.net
+--- Legit cases: reset to null ---
+string(15) "https://php.net"
--- Error cases ---
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a callable, function "nonexistent" not found or invalid function name
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|string|null, Closure given
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index dddd944882cdc..be17aa1f0eb0f 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -77,10 +77,6 @@ static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs,
"xmlExtFunctionTest: failed to get the internal object\n");
error = true;
}
- else if (intern->xpath_callbacks.mode == PHP_DOM_REG_FUNC_MODE_NONE) {
- zend_throw_error(NULL, "No callbacks were registered");
- error = true;
- }
}
if (error) {
@@ -381,7 +377,16 @@ PHP_METHOD(DOMXPath, evaluate)
PHP_METHOD(DOMXPath, registerPhpFunctions)
{
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
- php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, INTERNAL_FUNCTION_PARAM_PASSTHRU);
+
+ zend_string *name = NULL;
+ HashTable *callable_ht = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
}
/* }}} end dom_xpath_register_php_functions */
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 5ec9466f398d0..1b30e30aa53b9 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -33,10 +33,19 @@ static void xpath_callbacks_entry_dtor(zval *zv)
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)
{
- ALLOC_HASHTABLE(registry->functions);
- zend_hash_init(registry->functions, 0, NULL, xpath_callbacks_entry_dtor, false);
}
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry)
@@ -61,25 +70,48 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserC
PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry)
{
- if (registry->functions) {
- zend_hash_destroy(registry->functions);
- FREE_HASHTABLE(registry->functions);
+ 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_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)
+{
+ zval *entry;
+ ZEND_HASH_FOREACH_VAL(&ns->functions, entry) {
+ ZEND_ASSERT(Z_TYPE_P(entry) == IS_PTR);
+ zend_get_gc_buffer_add_fcc(gc_buffer, Z_PTR_P(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)
{
- zval *entry;
- ZEND_HASH_FOREACH_VAL(registry->functions, entry) {
- ZEND_ASSERT(Z_TYPE_P(entry) == IS_PTR);
- zend_get_gc_buffer_add_fcc(gc_buffer, Z_PTR_P(entry));
- } ZEND_HASH_FOREACH_END();
+ 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_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->mode == PHP_DOM_REG_FUNC_MODE_SET) {
+ 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);
@@ -94,27 +126,20 @@ PHP_DOM_EXPORT HashTable *php_dom_xpath_callbacks_get_gc_for_whole_object(php_do
}
}
-PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, INTERNAL_FUNCTION_PARAMETERS)
+static void php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath_callback_ns* ns, zend_string *name, const HashTable *callable_ht)
{
zval *entry, registered_value;
- zend_string *name = NULL;
- HashTable *ht = NULL;
-
- ZEND_PARSE_PARAMETERS_START(0, 1)
- Z_PARAM_OPTIONAL
- Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(ht, name)
- ZEND_PARSE_PARAMETERS_END();
- if (ht) {
+ if (callable_ht) {
zend_string *key;
- ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, entry) {
+ ZEND_HASH_FOREACH_STR_KEY_VAL(callable_ht, key, entry) {
zend_fcall_info_cache* fcc = emalloc(sizeof(zend_fcall_info));
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_THROWS();
+ return;
}
zend_fcc_addref(fcc);
@@ -123,24 +148,24 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_
if (!key) {
zend_string *str = zval_try_get_string(entry);
if (str) {
- zend_hash_update(registry->functions, str, ®istered_value);
+ zend_hash_update(&ns->functions, str, ®istered_value);
zend_string_release_ex(str, 0);
} else {
zend_fcc_dtor(fcc);
efree(fcc);
- RETURN_THROWS();
+ return;
}
} else {
if (ZSTR_LEN(key) == 0) {
zend_argument_value_error(1, "array key must not be empty");
zend_fcc_dtor(fcc);
efree(fcc);
- RETURN_THROWS();
+ return;
}
- zend_hash_update(registry->functions, key, ®istered_value);
+ zend_hash_update(&ns->functions, key, ®istered_value);
}
} ZEND_HASH_FOREACH_END();
- registry->mode = PHP_DOM_REG_FUNC_MODE_SET;
+ ns->mode = PHP_DOM_REG_FUNC_MODE_SET;
} else if (name) {
zend_fcall_info_cache* fcc = emalloc(sizeof(zend_fcall_info));
char *error;
@@ -150,97 +175,114 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_
zend_argument_type_error(1, "must be a callable, %s", error);
efree(fcc);
efree(error);
- RETURN_THROWS();
+ return;
}
zend_fcc_addref(fcc);
ZVAL_PTR(®istered_value, fcc);
- zend_hash_update(registry->functions, name, ®istered_value);
- registry->mode = PHP_DOM_REG_FUNC_MODE_SET;
+ zend_hash_update(&ns->functions, name, ®istered_value);
+ ns->mode = PHP_DOM_REG_FUNC_MODE_SET;
} else {
- registry->mode = PHP_DOM_REG_FUNC_MODE_ALL;
+ ns->mode = PHP_DOM_REG_FUNC_MODE_ALL;
}
}
-PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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)
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, zend_string *ns, zend_string *name, const HashTable *callable_ht)
{
- zval callback_retval;
- zval *params = NULL;
- zend_result result = FAILURE;
+ if (ns == NULL) {
+ if (!registry->php_ns) {
+ registry->php_ns = emalloc(sizeof(php_dom_xpath_callback_ns));
+ php_dom_xpath_callback_ns_ctor(registry->php_ns);
+ }
+ php_dom_xpath_callback_ns_update_method_handler(registry->php_ns, name, callable_ht);
+ } else {
+ abort(); // TODO
+ }
+}
- if (UNEXPECTED(num_args == 0)) {
- zend_throw_error(NULL, "Function name must be passed as the first argument");
- goto cleanup_no_obj;
+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;
}
- uint32_t param_count = num_args - 1;
- if (param_count > 0) {
- 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 transfered 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);
- }
- add_next_index_zval(param, &child);
+ 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 transfered 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);
}
- } else {
- ZVAL_EMPTY_ARRAY(param);
+ zend_hash_next_index_insert_new(Z_ARRVAL_P(param), &child);
}
+ } else {
+ ZVAL_EMPTY_ARRAY(param);
}
- break;
- default:
- ZVAL_STRING(param, (char *)xmlXPathCastToString(obj));
- break;
- }
- xmlXPathFreeObject(obj);
+ }
+ break;
+ default:
+ ZVAL_STRING(param, (char *)xmlXPathCastToString(obj));
+ break;
}
+ xmlXPathFreeObject(obj);
}
- /* Last element of the stack is the function name */
- xmlXPathObjectPtr obj = valuePop(ctxt);
- if (obj->stringval == NULL) {
- zend_type_error("Handler name must be a string");
- goto cleanup;
+ return params;
+}
+
+static void php_dom_xpath_callback_cleanup_args(zval *params, uint32_t param_count)
+{
+ if (params) {
+ for (uint32_t i = 0; i < param_count; i++) {
+ zval_ptr_dtor(¶ms[i]);
+ }
+ efree(params);
}
+}
- const char *function_name = (const char *) obj->stringval;
- size_t function_name_length = strlen(function_name);
+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 (xpath_callbacks->mode == PHP_DOM_REG_FUNC_MODE_ALL) {
+ 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;
@@ -253,17 +295,17 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
zend_result result = zend_call_function(&fci, NULL);
zend_string_release_ex(Z_STR(fci.function_name), false);
if (UNEXPECTED(result == FAILURE)) {
- goto cleanup;
+ return FAILURE;
}
} else {
- ZEND_ASSERT(xpath_callbacks->mode == PHP_DOM_REG_FUNC_MODE_SET);
+ ZEND_ASSERT(ns->mode == PHP_DOM_REG_FUNC_MODE_SET);
- zval *fcc_wrapper = zend_hash_str_find(xpath_callbacks->functions, function_name, function_name_length);
+ 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);
- goto cleanup;
+ return FAILURE;
}
}
@@ -275,7 +317,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
xpath_callbacks->node_list = zend_new_array(0);
}
Z_ADDREF_P(&callback_retval);
- zend_hash_next_index_insert(xpath_callbacks->node_list, &callback_retval);
+ zend_hash_next_index_insert_new(xpath_callbacks->node_list, &callback_retval);
obj = Z_DOMOBJ_P(&callback_retval);
nodep = dom_object_get_node(obj);
valuePush(ctxt, xmlXPathNewNodeSet(nodep));
@@ -284,7 +326,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
} else if (Z_TYPE(callback_retval) == IS_OBJECT) {
zend_type_error("Only objects that are instances of DOMNode can be converted to an XPath expression");
zval_ptr_dtor(&callback_retval);
- goto cleanup;
+ return FAILURE;
} else {
zend_string *str = zval_get_string(&callback_retval);
valuePush(ctxt, xmlXPathNewString((xmlChar *) ZSTR_VAL(str)));
@@ -293,21 +335,41 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
zval_ptr_dtor(&callback_retval);
}
- result = SUCCESS;
+ return SUCCESS;
+}
+
+PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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 (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(params, param_count);
cleanup_no_obj:
if (UNEXPECTED(result != SUCCESS)) {
/* Push sentinel value */
valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
}
- if (params) {
- for (uint32_t i = 0; i < param_count; i++) {
- zval_ptr_dtor(¶ms[i]);
- }
- efree(params);
- }
return result;
}
diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h
index 1bde91e4ae751..3a2b1ff79f2cb 100644
--- a/ext/dom/xpath_callbacks.h
+++ b/ext/dom/xpath_callbacks.h
@@ -36,8 +36,13 @@ typedef enum {
typedef void (*php_dom_xpath_callbacks_proxy_factory)(xmlNodePtr node, zval *proxy, dom_object *intern, xmlXPathParserContextPtr ctxt);
typedef struct {
+ HashTable functions;
php_dom_register_functions_mode mode;
- HashTable *functions;
+} php_dom_xpath_callback_ns;
+
+typedef struct {
+ php_dom_xpath_callback_ns *php_ns;
+ HashTable *namespaces;
HashTable *node_list;
} php_dom_xpath_callbacks;
@@ -47,7 +52,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callba
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer);
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);
-PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, INTERNAL_FUNCTION_PARAMETERS);
+PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, zend_string *ns, zend_string *name, const HashTable *callable_ht);
PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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);
#endif
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 4ebb0514a7b51..78dc5ecc0e57b 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -87,10 +87,6 @@ static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_d
"xsltExtFunctionTest: failed to get the internal object\n");
error = true;
}
- else if (intern->xpath_callbacks.mode == PHP_DOM_REG_FUNC_MODE_NONE) {
- zend_throw_error(NULL, "No callbacks were registered");
- error = true;
- }
}
}
@@ -575,7 +571,16 @@ PHP_METHOD(XSLTProcessor, removeParameter)
PHP_METHOD(XSLTProcessor, registerPHPFunctions)
{
xsl_object *intern = Z_XSL_P(ZEND_THIS);
- php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, INTERNAL_FUNCTION_PARAM_PASSTHRU);
+
+ zend_string *name = NULL;
+ HashTable *callable_ht = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
}
/* }}} end XSLTProcessor::registerPHPFunctions(); */
From e8b9b5e00e8fd510d14ee4dc8c0402096174c9d1 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 30 Nov 2023 23:08:07 +0100
Subject: [PATCH 03/12] Implement namespaced functions for DOM
---
ext/dom/php_dom.stub.php | 2 +
ext/dom/php_dom_arginfo.h | 13 ++-
ext/dom/tests/DOMXPath_callables.phpt | 16 ++-
ext/dom/tests/registerPhpFunctionsNS.phpt | 91 +++++++++++++++++
ext/dom/xpath.c | 80 ++++++++++++---
ext/dom/xpath_callbacks.c | 111 ++++++++++++++++++---
ext/dom/xpath_callbacks.h | 12 ++-
ext/xsl/tests/XSLTProcessor_callables.phpt | 16 ++-
ext/xsl/xsltprocessor.c | 12 ++-
9 files changed, 317 insertions(+), 36 deletions(-)
create mode 100644 ext/dom/tests/registerPhpFunctionsNS.phpt
diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php
index db86fe249b8ec..4c06888d0c253 100644
--- a/ext/dom/php_dom.stub.php
+++ b/ext/dom/php_dom.stub.php
@@ -932,6 +932,8 @@ public function registerNamespace(string $prefix, string $namespace): bool {}
/** @tentative-return-type */
public function registerPhpFunctions(string|array|null $restrict = null): void {}
+
+ public function registerPhpFunctionsNS(string $namespace, string|array $restrict): void {}
}
#endif
diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h
index 3a2ec9eddc034..7e95f780237aa 100644
--- a/ext/dom/php_dom_arginfo.h
+++ b/ext/dom/php_dom_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 5512165ddaad08287561abac2a325e2aab3c6188 */
+ * Stub hash: 8a158902b843f63a3da76d4e8b12669c186976f2 */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@@ -451,6 +451,13 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMXPath_registe
ZEND_END_ARG_INFO()
#endif
+#if defined(LIBXML_XPATH_ENABLED)
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMXPath_registerPhpFunctionsNS, 0, 2, IS_VOID, 0)
+ ZEND_ARG_TYPE_INFO(0, namespace, IS_STRING, 0)
+ ZEND_ARG_TYPE_MASK(0, restrict, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
+ZEND_END_ARG_INFO()
+#endif
+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_DOM_Document_createAttribute, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, localName, IS_STRING, 0)
ZEND_END_ARG_INFO()
@@ -738,6 +745,9 @@ ZEND_METHOD(DOMXPath, registerNamespace);
#if defined(LIBXML_XPATH_ENABLED)
ZEND_METHOD(DOMXPath, registerPhpFunctions);
#endif
+#if defined(LIBXML_XPATH_ENABLED)
+ZEND_METHOD(DOMXPath, registerPhpFunctionsNS);
+#endif
ZEND_METHOD(DOM_Document, createAttribute);
ZEND_METHOD(DOM_Document, createAttributeNS);
ZEND_METHOD(DOM_Document, createCDATASection);
@@ -1014,6 +1024,7 @@ static const zend_function_entry class_DOMXPath_methods[] = {
ZEND_ME(DOMXPath, query, arginfo_class_DOMXPath_query, ZEND_ACC_PUBLIC)
ZEND_ME(DOMXPath, registerNamespace, arginfo_class_DOMXPath_registerNamespace, ZEND_ACC_PUBLIC)
ZEND_ME(DOMXPath, registerPhpFunctions, arginfo_class_DOMXPath_registerPhpFunctions, ZEND_ACC_PUBLIC)
+ ZEND_ME(DOMXPath, registerPhpFunctionsNS, arginfo_class_DOMXPath_registerPhpFunctionsNS, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
#endif
diff --git a/ext/dom/tests/DOMXPath_callables.phpt b/ext/dom/tests/DOMXPath_callables.phpt
index aa45de536222a..d43c85158b48f 100644
--- a/ext/dom/tests/DOMXPath_callables.phpt
+++ b/ext/dom/tests/DOMXPath_callables.phpt
@@ -109,6 +109,18 @@ try {
echo $e->getMessage(), "\n";
}
+try {
+ $xpath->registerPhpFunctions(["\0" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions("");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
?>
--EXPECT--
--- Legit cases: none ---
@@ -131,4 +143,6 @@ DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|
Object of class Closure could not be converted to string
Object of class Closure could not be converted to string
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) array key must not be empty
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a valid callback name
diff --git a/ext/dom/tests/registerPhpFunctionsNS.phpt b/ext/dom/tests/registerPhpFunctionsNS.phpt
new file mode 100644
index 0000000000000..073080261b8d5
--- /dev/null
+++ b/ext/dom/tests/registerPhpFunctionsNS.phpt
@@ -0,0 +1,91 @@
+--TEST--
+registerPhpFunctionsNS() function
+--EXTENSIONS--
+dom
+--FILE--
+loadHTML('hello');
+
+$xpath = new DOMXPath($doc);
+
+echo "--- Error cases ---\n";
+
+try {
+ $xpath->registerPhpFunctionsNS('http://php.net/xpath', ['strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionsNS("\0", ['strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: string callable ---\n";
+
+$xpath->registerNamespace('foo', 'urn:foo');
+$xpath->registerPhpFunctionsNS('urn:foo', 'strtolower');
+var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));
+
+echo "--- Legit cases: string callable in array ---\n";
+
+$xpath->registerPhpFunctionsNS('urn:foo', ['strtoupper']);
+var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]'));
+
+echo "--- Legit cases: callable in array ---\n";
+
+$xpath->registerPhpFunctionsNS('urn:foo', ['test' => 'var_dump']);
+$xpath->query('//a[foo:test(string(@href))]');
+
+echo "--- Legit cases: multiple namespaces ---\n";
+
+$xpath->registerNamespace('bar', 'urn:bar');
+$xpath->registerPhpFunctionsNS('urn:bar', ['test' => 'strtolower']);
+var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
+
+?>
+--EXPECT--
+--- Error cases ---
+DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xpath" because it is reserved for PHP
+DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
+DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
+DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
+Object of class Closure could not be converted to string
+--- Legit cases: string callable ---
+object(DOMNodeList)#6 (1) {
+ ["length"]=>
+ int(1)
+}
+--- Legit cases: string callable in array ---
+object(DOMNodeList)#6 (1) {
+ ["length"]=>
+ int(0)
+}
+--- Legit cases: callable in array ---
+string(15) "https://PHP.net"
+--- Legit cases: multiple namespaces ---
+object(DOMNodeList)#4 (1) {
+ ["length"]=>
+ int(1)
+}
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index be17aa1f0eb0f..b49956ac2d323 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -61,28 +61,30 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in
php_dom_create_object(node, child, intern);
}
-static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
{
- bool error = false;
- dom_xpath_object *intern;
-
- if (! zend_is_executing()) {
+ if (!zend_is_executing()) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: Function called from outside of PHP\n");
- error = true;
} else {
- intern = (dom_xpath_object *) ctxt->context->userData;
+ dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
if (intern == NULL) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: failed to get the internal object\n");
- error = true;
+ return NULL;
}
+ return intern;
}
+ return NULL;
+}
- if (error) {
+static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+{
+ dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
+ if (!intern) {
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
- php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
+ php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
}
}
/* }}} */
@@ -99,6 +101,16 @@ static void dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt, int
}
/* }}} */
+static void dom_xpath_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
+{
+ dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
+ if (!intern) {
+ php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
+ } else {
+ php_dom_xpath_callbacks_call_custom_ns(&intern->xpath_callbacks, ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET, &intern->dom, dom_xpath_proxy_factory);
+ }
+}
+
/* {{{ */
PHP_METHOD(DOMXPath, __construct)
{
@@ -378,18 +390,60 @@ PHP_METHOD(DOMXPath, registerPhpFunctions)
{
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
- zend_string *name = NULL;
+ zend_string *callable_name = NULL;
HashTable *callable_ht = NULL;
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
- Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
+ Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, callable_name)
ZEND_PARSE_PARAMETERS_END();
- php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
+ php_dom_xpath_callbacks_update_method_handler(
+ &intern->xpath_callbacks,
+ intern->dom.ptr,
+ NULL,
+ callable_name,
+ callable_ht,
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
+ NULL
+ );
}
/* }}} end dom_xpath_register_php_functions */
+static void dom_xpath_register_func_in_ctx(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name)
+{
+ xmlXPathRegisterFuncNS(ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
+}
+
+PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
+{
+ dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
+
+ zend_string *namespace;
+ zend_string *callable_name;
+ HashTable *callable_ht;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_PATH_STR(namespace)
+ Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (zend_string_equals_literal(namespace, "http://php.net/xpath")) { // TODO: this is different for XSL!!!
+ zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved for PHP");
+ RETURN_THROWS();
+ }
+
+ php_dom_xpath_callbacks_update_method_handler(
+ &intern->xpath_callbacks,
+ intern->dom.ptr,
+ namespace,
+ callable_name,
+ callable_ht,
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
+ dom_xpath_register_func_in_ctx
+ );
+}
+
#endif /* LIBXML_XPATH_ENABLED */
#endif
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 1b30e30aa53b9..50f2858cf2647 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -126,7 +126,37 @@ PHP_DOM_EXPORT HashTable *php_dom_xpath_callbacks_get_gc_for_whole_object(php_do
}
}
-static void php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath_callback_ns* ns, zend_string *name, const HashTable *callable_ht)
+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((xmlChar *) 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)
+{
+ if (!php_dom_xpath_is_callback_name_valid(name, name_validation)) {
+ zend_argument_value_error(1, "must be an array containing valid callback names");
+ return false;
+ }
+ return true;
+}
+
+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;
@@ -139,7 +169,7 @@ static void php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath_callba
zend_argument_type_error(1, "must be an array with valid callbacks as values, %s", error);
efree(fcc);
efree(error);
- return;
+ return FAILURE;
}
zend_fcc_addref(fcc);
@@ -147,26 +177,35 @@ static void php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath_callba
if (!key) {
zend_string *str = zval_try_get_string(entry);
- if (str) {
+ if (str && php_dom_xpath_is_callback_name_valid_and_throw(str, name_validation)) {
zend_hash_update(&ns->functions, str, ®istered_value);
- zend_string_release_ex(str, 0);
+ if (register_func) {
+ register_func(ctxt, namespace, str);
+ }
+ zend_string_release_ex(str, false);
} else {
zend_fcc_dtor(fcc);
efree(fcc);
- return;
+ return FAILURE;
}
} else {
- if (ZSTR_LEN(key) == 0) {
- zend_argument_value_error(1, "array key must not be empty");
+ if (!php_dom_xpath_is_callback_name_valid_and_throw(key, name_validation)) {
zend_fcc_dtor(fcc);
efree(fcc);
- return;
+ return FAILURE;
}
zend_hash_update(&ns->functions, key, ®istered_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(zend_fcall_info));
char *error;
zval tmp;
@@ -175,27 +214,42 @@ static void php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath_callba
zend_argument_type_error(1, "must be a callable, %s", error);
efree(fcc);
efree(error);
- return;
+ return FAILURE;
}
zend_fcc_addref(fcc);
ZVAL_PTR(®istered_value, fcc);
zend_hash_update(&ns->functions, name, ®istered_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;
}
-PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, zend_string *ns, zend_string *name, const HashTable *callable_ht)
+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)
{
if (ns == NULL) {
if (!registry->php_ns) {
registry->php_ns = emalloc(sizeof(php_dom_xpath_callback_ns));
php_dom_xpath_callback_ns_ctor(registry->php_ns);
}
- php_dom_xpath_callback_ns_update_method_handler(registry->php_ns, name, callable_ht);
+ return php_dom_xpath_callback_ns_update_method_handler(registry->php_ns, ctxt, ns, name, callable_ht, name_validation, register_func);
} else {
- abort(); // TODO
+ 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(php_dom_xpath_callback_ns));
+ php_dom_xpath_callback_ns_ctor(namespace);
+ zend_hash_add_new_ptr(registry->namespaces, ns, namespace);
+ }
+ return php_dom_xpath_callback_ns_update_method_handler(namespace, ctxt, ns, name, callable_ht, name_validation, register_func);
}
}
@@ -338,7 +392,7 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat
return SUCCESS;
}
-PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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)
+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;
@@ -360,7 +414,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
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);
+ result = php_dom_xpath_callback_dispatch(xpath_callbacks, xpath_callbacks->php_ns, ctxt, params, param_count, function_name, function_name_length);
cleanup:
xmlXPathFreeObject(obj);
@@ -371,7 +425,34 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(php_dom_xpath_callbacks
valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
}
- return result;
+ 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(params, param_count);
+ if (UNEXPECTED(result != SUCCESS)) {
+ /* Push sentinel value */
+ valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));
+ }
+
+ return result;
}
#endif
diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h
index 3a2b1ff79f2cb..da41c82a7852f 100644
--- a/ext/dom/xpath_callbacks.h
+++ b/ext/dom/xpath_callbacks.h
@@ -34,6 +34,7 @@ typedef enum {
} php_dom_xpath_nodeset_evaluation_mode;
typedef void (*php_dom_xpath_callbacks_proxy_factory)(xmlNodePtr node, zval *proxy, dom_object *intern, xmlXPathParserContextPtr ctxt);
+typedef void (*php_dom_xpath_callbacks_register_func_ctx)(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name);
typedef struct {
HashTable functions;
@@ -46,15 +47,20 @@ typedef struct {
HashTable *node_list;
} php_dom_xpath_callbacks;
+typedef enum {
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
+} php_dom_xpath_callback_name_validation;
+
PHP_DOM_EXPORT void php_dom_xpath_callbacks_ctor(php_dom_xpath_callbacks *registry);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer);
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);
-PHP_DOM_EXPORT void php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, zend_string *ns, zend_string *name, const HashTable *callable_ht);
-PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call(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);
+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_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);
+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);
#endif
#endif
-
diff --git a/ext/xsl/tests/XSLTProcessor_callables.phpt b/ext/xsl/tests/XSLTProcessor_callables.phpt
index 1603fd4436451..a845d77feec63 100644
--- a/ext/xsl/tests/XSLTProcessor_callables.phpt
+++ b/ext/xsl/tests/XSLTProcessor_callables.phpt
@@ -119,6 +119,18 @@ try {
echo $e->getMessage(), "\n";
}
+try {
+ $proc->registerPhpFunctions(["\0" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions("");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
?>
--EXPECT--
--- Legit cases: none ---
@@ -147,4 +159,6 @@ XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be of type
Object of class Closure could not be converted to string
Object of class Closure could not be converted to string
XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) array key must not be empty
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a valid callback name
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 78dc5ecc0e57b..956b9d90c633b 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -93,7 +93,7 @@ static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_d
if (error) {
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
- php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, (dom_object *) intern->doc, xsl_proxy_factory);
+ php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, (dom_object *) intern->doc, xsl_proxy_factory);
}
}
/* }}} */
@@ -580,7 +580,15 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctions)
Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
ZEND_PARSE_PARAMETERS_END();
- php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
+ php_dom_xpath_callbacks_update_method_handler(
+ &intern->xpath_callbacks,
+ NULL,
+ NULL,
+ name,
+ callable_ht,
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
+ NULL
+ );
}
/* }}} end XSLTProcessor::registerPHPFunctions(); */
From 70817b98f282bea9a7a7e502c9ee2beb6e825a22 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Fri, 1 Dec 2023 00:11:58 +0100
Subject: [PATCH 04/12] Implement namespaced functions for XSL
---
ext/dom/tests/registerPhpFunctionsNS.phpt | 4 +-
ext/dom/xpath.c | 6 +-
ext/dom/xpath_callbacks.c | 25 +++--
ext/dom/xpath_callbacks.h | 3 +-
ext/xsl/php_xsl.stub.php | 2 +
ext/xsl/php_xsl_arginfo.h | 9 +-
ext/xsl/tests/registerPHPFunctionsNS.phpt | 120 ++++++++++++++++++++++
ext/xsl/xsltprocessor.c | 69 +++++++++++--
8 files changed, 215 insertions(+), 23 deletions(-)
create mode 100644 ext/xsl/tests/registerPHPFunctionsNS.phpt
diff --git a/ext/dom/tests/registerPhpFunctionsNS.phpt b/ext/dom/tests/registerPhpFunctionsNS.phpt
index 073080261b8d5..d05a430d2b753 100644
--- a/ext/dom/tests/registerPhpFunctionsNS.phpt
+++ b/ext/dom/tests/registerPhpFunctionsNS.phpt
@@ -55,7 +55,7 @@ var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]')
echo "--- Legit cases: callable in array ---\n";
-$xpath->registerPhpFunctionsNS('urn:foo', ['test' => 'var_dump']);
+$xpath->registerPhpFunctionsNS('urn:foo', ['test' => var_dump(...)]);
$xpath->query('//a[foo:test(string(@href))]');
echo "--- Legit cases: multiple namespaces ---\n";
@@ -85,7 +85,7 @@ object(DOMNodeList)#6 (1) {
--- Legit cases: callable in array ---
string(15) "https://PHP.net"
--- Legit cases: multiple namespaces ---
-object(DOMNodeList)#4 (1) {
+object(DOMNodeList)#7 (1) {
["length"]=>
int(1)
}
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index b49956ac2d323..b88969c00d7ab 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -410,9 +410,9 @@ PHP_METHOD(DOMXPath, registerPhpFunctions)
}
/* }}} end dom_xpath_register_php_functions */
-static void dom_xpath_register_func_in_ctx(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name)
+static void dom_xpath_register_func_in_ctx(void *ctxt, const zend_string *ns, const zend_string *name)
{
- xmlXPathRegisterFuncNS(ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
+ xmlXPathRegisterFuncNS((xmlXPathContextPtr) ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
}
PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
@@ -428,7 +428,7 @@ PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
ZEND_PARSE_PARAMETERS_END();
- if (zend_string_equals_literal(namespace, "http://php.net/xpath")) { // TODO: this is different for XSL!!!
+ if (zend_string_equals_literal(namespace, "http://php.net/xpath")) {
zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved for PHP");
RETURN_THROWS();
}
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 50f2858cf2647..8b539f98d4a74 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -76,7 +76,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *regist
}
if (registry->namespaces) {
php_dom_xpath_callback_ns *ns;
- ZEND_HASH_FOREACH_PTR(registry->namespaces, ns) {
+ ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
php_dom_xpath_callback_ns_dtor(ns);
efree(ns);
} ZEND_HASH_FOREACH_END();
@@ -89,10 +89,9 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *regist
static void php_dom_xpath_callback_ns_get_gc(php_dom_xpath_callback_ns *ns, zend_get_gc_buffer *gc_buffer)
{
- zval *entry;
- ZEND_HASH_FOREACH_VAL(&ns->functions, entry) {
- ZEND_ASSERT(Z_TYPE_P(entry) == IS_PTR);
- zend_get_gc_buffer_add_fcc(gc_buffer, Z_PTR_P(entry));
+ 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();
}
@@ -103,7 +102,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *regi
}
if (registry->namespaces) {
php_dom_xpath_callback_ns *ns;
- ZEND_HASH_FOREACH_PTR(registry->namespaces, ns) {
+ ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
php_dom_xpath_callback_ns_get_gc(ns, gc_buffer);
} ZEND_HASH_FOREACH_END();
}
@@ -156,6 +155,20 @@ static bool php_dom_xpath_is_callback_name_valid_and_throw(const zend_string *na
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;
diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h
index da41c82a7852f..332c8c7cd56f4 100644
--- a/ext/dom/xpath_callbacks.h
+++ b/ext/dom/xpath_callbacks.h
@@ -34,7 +34,7 @@ typedef enum {
} php_dom_xpath_nodeset_evaluation_mode;
typedef void (*php_dom_xpath_callbacks_proxy_factory)(xmlNodePtr node, zval *proxy, dom_object *intern, xmlXPathParserContextPtr ctxt);
-typedef void (*php_dom_xpath_callbacks_register_func_ctx)(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name);
+typedef void (*php_dom_xpath_callbacks_register_func_ctx)(void *ctxt, const zend_string *ns, const zend_string *name);
typedef struct {
HashTable functions;
@@ -58,6 +58,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callba
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args);
PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer);
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);
+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);
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_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);
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);
diff --git a/ext/xsl/php_xsl.stub.php b/ext/xsl/php_xsl.stub.php
index 9394562425d78..ac10e2e48f8cb 100644
--- a/ext/xsl/php_xsl.stub.php
+++ b/ext/xsl/php_xsl.stub.php
@@ -114,6 +114,8 @@ public function hasExsltSupport(): bool {}
/** @tentative-return-type */
public function registerPHPFunctions(array|string|null $functions = null): void {}
+ public function registerPHPFunctionsNS(string $namespace, array|string $functions): void {}
+
/** @return true */
public function setProfiling(?string $filename) {} // TODO make return type void
diff --git a/ext/xsl/php_xsl_arginfo.h b/ext/xsl/php_xsl_arginfo.h
index 92a7ab61e7810..893de080c6dab 100644
--- a/ext/xsl/php_xsl_arginfo.h
+++ b/ext/xsl/php_xsl_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 87ea452722956b6cfe46458e7fcd97f0bcfb767b */
+ * Stub hash: 61172e9a28769cacef9f43181cbeafdb42a87cf2 */
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_importStylesheet, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, stylesheet, IS_OBJECT, 0)
@@ -42,6 +42,11 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_re
ZEND_ARG_TYPE_MASK(0, functions, MAY_BE_ARRAY|MAY_BE_STRING|MAY_BE_NULL, "null")
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_registerPHPFunctionsNS, 0, 2, IS_VOID, 0)
+ ZEND_ARG_TYPE_INFO(0, namespace, IS_STRING, 0)
+ ZEND_ARG_TYPE_MASK(0, functions, MAY_BE_ARRAY|MAY_BE_STRING, NULL)
+ZEND_END_ARG_INFO()
+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_XSLTProcessor_setProfiling, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 1)
ZEND_END_ARG_INFO()
@@ -63,6 +68,7 @@ ZEND_METHOD(XSLTProcessor, getParameter);
ZEND_METHOD(XSLTProcessor, removeParameter);
ZEND_METHOD(XSLTProcessor, hasExsltSupport);
ZEND_METHOD(XSLTProcessor, registerPHPFunctions);
+ZEND_METHOD(XSLTProcessor, registerPHPFunctionsNS);
ZEND_METHOD(XSLTProcessor, setProfiling);
ZEND_METHOD(XSLTProcessor, setSecurityPrefs);
ZEND_METHOD(XSLTProcessor, getSecurityPrefs);
@@ -78,6 +84,7 @@ static const zend_function_entry class_XSLTProcessor_methods[] = {
ZEND_ME(XSLTProcessor, removeParameter, arginfo_class_XSLTProcessor_removeParameter, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, hasExsltSupport, arginfo_class_XSLTProcessor_hasExsltSupport, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, registerPHPFunctions, arginfo_class_XSLTProcessor_registerPHPFunctions, ZEND_ACC_PUBLIC)
+ ZEND_ME(XSLTProcessor, registerPHPFunctionsNS, arginfo_class_XSLTProcessor_registerPHPFunctionsNS, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, setProfiling, arginfo_class_XSLTProcessor_setProfiling, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, setSecurityPrefs, arginfo_class_XSLTProcessor_setSecurityPrefs, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, getSecurityPrefs, arginfo_class_XSLTProcessor_getSecurityPrefs, ZEND_ACC_PUBLIC)
diff --git a/ext/xsl/tests/registerPHPFunctionsNS.phpt b/ext/xsl/tests/registerPHPFunctionsNS.phpt
new file mode 100644
index 0000000000000..384f0a16936d5
--- /dev/null
+++ b/ext/xsl/tests/registerPHPFunctionsNS.phpt
@@ -0,0 +1,120 @@
+--TEST--
+registerPHPFunctionsNS() function
+--EXTENSIONS--
+xsl
+--FILE--
+loadXML('
+
+ '
+ . implode('', array_map(fn($input) => '', $inputs)) .
+ '
+ ');
+
+ $proc = new XSLTProcessor();
+ $proc->importStylesheet($xsl);
+ return $proc;
+}
+
+$inputdom = new DomDocument();
+$inputdom->loadXML('hello');
+
+echo "--- Legit cases: none ---\n";
+
+$proc = createProcessor(["foo:var_dump(string(@href))"]);
+try {
+ $proc->transformToXml($inputdom);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "--- Legit cases: string callable ---\n";
+
+$proc = createProcessor(["foo:var_dump(string(@href))"]);
+$proc->registerPHPFunctionsNS('urn:foo', 'var_dump');
+$proc->transformToXml($inputdom);
+
+echo "--- Legit cases: string callable in array ---\n";
+
+$proc = createProcessor(["foo:var_dump(string(@href))"]);
+$proc->registerPHPFunctionsNS('urn:foo', ['var_dump']);
+$proc->transformToXml($inputdom);
+
+echo "--- Legit cases: callable in array ---\n";
+
+$proc = createProcessor(["foo:test(string(@href))"]);
+$proc->registerPHPFunctionsNS('urn:foo', ['test' => var_dump(...)]);
+$proc->transformToXml($inputdom);
+
+echo "--- Legit cases: multiple namespaces ---\n";
+
+$proc = createProcessor(["foo:test(string(@href))", "bar:test(string(@href))"]);
+$proc->registerPHPFunctionsNS('urn:foo', ['test' => strrev(...)]);
+$proc->registerPHPFunctionsNS('urn:bar', ['test' => strtoupper(...)]);
+var_dump($proc->transformToXml($inputdom));
+
+echo "--- Error cases ---\n";
+
+try {
+ createProcessor([])->registerPhpFunctionsNS('http://php.net/xsl', ['strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionsNS("\0", ['strtolower']);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+--- Legit cases: none ---
+
+Warning: XSLTProcessor::transformToXml(): xmlXPathCompOpEval: function var_dump not found in %s on line %d
+
+Warning: XSLTProcessor::transformToXml(): Unregistered function in %s on line %d
+
+Warning: XSLTProcessor::transformToXml(): runtime error: file %s line 6 element value-of in %s on line %d
+
+Warning: XSLTProcessor::transformToXml(): XPath evaluation returned no result. in %s on line %d
+--- Legit cases: string callable ---
+string(15) "https://php.net"
+--- Legit cases: string callable in array ---
+string(15) "https://php.net"
+--- Legit cases: callable in array ---
+string(15) "https://php.net"
+--- Legit cases: multiple namespaces ---
+string(53) "
+ten.php//:sptthHTTPS://PHP.NET
+"
+--- Error cases ---
+XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xsl" because it is reserved for PHP
+XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
+Object of class Closure could not be converted to string
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 956b9d90c633b..3c7f49a4e887c 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -65,32 +65,34 @@ static void xsl_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern,
php_dom_create_object(node, child, intern);
}
-static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+static xsl_object *xsl_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
{
- bool error = false;
- xsl_object *intern;
-
- if (! zend_is_executing()) {
+ if (!zend_is_executing()) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: Function called from outside of PHP\n");
- error = true;
} else {
xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt);
if (tctxt == NULL) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: failed to get the transformation context\n");
- error = true;
} else {
- intern = (xsl_object*)tctxt->_private;
+ xsl_object *intern = (xsl_object *) tctxt->_private;
if (intern == NULL) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: failed to get the internal object\n");
- error = true;
+ return NULL;
}
+ return intern;
}
}
- if (error) {
+ return NULL;
+}
+
+static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
+{
+ xsl_object *intern = xsl_ext_fetch_intern(ctxt);
+ if (!intern) {
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, (dom_object *) intern->doc, xsl_proxy_factory);
@@ -110,6 +112,16 @@ void xsl_ext_function_object_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{
}
/* }}} */
+static void xsl_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
+{
+ xsl_object *intern = xsl_ext_fetch_intern(ctxt);
+ if (!intern) {
+ php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
+ } else {
+ php_dom_xpath_callbacks_call_custom_ns(&intern->xpath_callbacks, ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET, (dom_object *) intern->doc, xsl_proxy_factory);
+ }
+}
+
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#
Since:
*/
@@ -193,6 +205,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
}
/* }}} end XSLTProcessor::importStylesheet */
+static void php_xsl_delayed_lib_registration(void *ctxt, const zend_string *ns, const zend_string *name)
+{
+ xsltTransformContextPtr xsl = (xsltTransformContextPtr) ctxt;
+ xsltRegisterExtFunction(xsl, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), xsl_ext_function_trampoline);
+}
+
static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStylesheetPtr style, zval *docp) /* {{{ */
{
xmlDocPtr newdocp = NULL;
@@ -299,6 +317,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
}
}
+ php_dom_xpath_callbacks_delayed_lib_registration(&intern->xpath_callbacks, ctxt, php_xsl_delayed_lib_registration);
+
if (secPrefsError == 1) {
php_error_docref(NULL, E_WARNING, "Can't set libxslt security properties, not doing transformation for security reasons");
} else {
@@ -592,6 +612,35 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctions)
}
/* }}} end XSLTProcessor::registerPHPFunctions(); */
+PHP_METHOD(XSLTProcessor, registerPHPFunctionsNS)
+{
+ xsl_object *intern = Z_XSL_P(ZEND_THIS);
+
+ zend_string *namespace;
+ zend_string *callable_name;
+ HashTable *callable_ht;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_PATH_STR(namespace)
+ Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (zend_string_equals_literal(namespace, "http://php.net/xsl")) {
+ zend_argument_value_error(1, "must not be \"http://php.net/xsl\" because it is reserved for PHP");
+ RETURN_THROWS();
+ }
+
+ php_dom_xpath_callbacks_update_method_handler(
+ &intern->xpath_callbacks,
+ NULL,
+ namespace,
+ callable_name,
+ callable_ht,
+ PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
+ NULL
+ );
+}
+
/* {{{ */
PHP_METHOD(XSLTProcessor, setProfiling)
{
From ce5ab0616999c96b90ca6556c3e69b2442dc9274 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Fri, 29 Dec 2023 01:10:08 +0100
Subject: [PATCH 05/12] Update to the new registerPhpFunctionNS proposal
---
ext/dom/php_dom.stub.php | 2 +-
ext/dom/php_dom_arginfo.h | 13 +-
ext/dom/tests/registerPhpFunctionNS.phpt | 137 ++++++++++++++++++++++
ext/dom/tests/registerPhpFunctionsNS.phpt | 91 --------------
ext/dom/xpath.c | 19 +--
ext/dom/xpath_callbacks.c | 47 ++++++--
ext/dom/xpath_callbacks.h | 1 +
ext/xsl/php_xsl.stub.php | 2 +-
ext/xsl/php_xsl_arginfo.h | 13 +-
ext/xsl/tests/registerPHPFunctionsNS.phpt | 95 ++++++++++-----
ext/xsl/xsltprocessor.c | 19 +--
11 files changed, 280 insertions(+), 159 deletions(-)
create mode 100644 ext/dom/tests/registerPhpFunctionNS.phpt
delete mode 100644 ext/dom/tests/registerPhpFunctionsNS.phpt
diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php
index 4c06888d0c253..bda09872694a9 100644
--- a/ext/dom/php_dom.stub.php
+++ b/ext/dom/php_dom.stub.php
@@ -933,7 +933,7 @@ public function registerNamespace(string $prefix, string $namespace): bool {}
/** @tentative-return-type */
public function registerPhpFunctions(string|array|null $restrict = null): void {}
- public function registerPhpFunctionsNS(string $namespace, string|array $restrict): void {}
+ public function registerPhpFunctionNS(string $namespaceURI, string $name, callable $callable): void {}
}
#endif
diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h
index 7e95f780237aa..6996a47f82c4b 100644
--- a/ext/dom/php_dom_arginfo.h
+++ b/ext/dom/php_dom_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 8a158902b843f63a3da76d4e8b12669c186976f2 */
+ * Stub hash: 184308dfd1a133145d170c467e7600a12b14e327 */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@@ -452,9 +452,10 @@ ZEND_END_ARG_INFO()
#endif
#if defined(LIBXML_XPATH_ENABLED)
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMXPath_registerPhpFunctionsNS, 0, 2, IS_VOID, 0)
- ZEND_ARG_TYPE_INFO(0, namespace, IS_STRING, 0)
- ZEND_ARG_TYPE_MASK(0, restrict, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMXPath_registerPhpFunctionNS, 0, 3, IS_VOID, 0)
+ ZEND_ARG_TYPE_INFO(0, namespaceURI, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, callable, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()
#endif
@@ -746,7 +747,7 @@ ZEND_METHOD(DOMXPath, registerNamespace);
ZEND_METHOD(DOMXPath, registerPhpFunctions);
#endif
#if defined(LIBXML_XPATH_ENABLED)
-ZEND_METHOD(DOMXPath, registerPhpFunctionsNS);
+ZEND_METHOD(DOMXPath, registerPhpFunctionNS);
#endif
ZEND_METHOD(DOM_Document, createAttribute);
ZEND_METHOD(DOM_Document, createAttributeNS);
@@ -1024,7 +1025,7 @@ static const zend_function_entry class_DOMXPath_methods[] = {
ZEND_ME(DOMXPath, query, arginfo_class_DOMXPath_query, ZEND_ACC_PUBLIC)
ZEND_ME(DOMXPath, registerNamespace, arginfo_class_DOMXPath_registerNamespace, ZEND_ACC_PUBLIC)
ZEND_ME(DOMXPath, registerPhpFunctions, arginfo_class_DOMXPath_registerPhpFunctions, ZEND_ACC_PUBLIC)
- ZEND_ME(DOMXPath, registerPhpFunctionsNS, arginfo_class_DOMXPath_registerPhpFunctionsNS, ZEND_ACC_PUBLIC)
+ ZEND_ME(DOMXPath, registerPhpFunctionNS, arginfo_class_DOMXPath_registerPhpFunctionNS, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
#endif
diff --git a/ext/dom/tests/registerPhpFunctionNS.phpt b/ext/dom/tests/registerPhpFunctionNS.phpt
new file mode 100644
index 0000000000000..72410c1d0db51
--- /dev/null
+++ b/ext/dom/tests/registerPhpFunctionNS.phpt
@@ -0,0 +1,137 @@
+--TEST--
+registerPhpFunctionNS() function
+--EXTENSIONS--
+dom
+--FILE--
+state[] = [$name, $arguments[0]];
+ return $arguments[0];
+ }
+}
+
+$doc = new DOMDocument();
+$doc->loadHTML('hello');
+
+$xpath = new DOMXPath($doc);
+
+echo "--- Error cases ---\n";
+
+try {
+ $xpath->registerPhpFunctionNS('http://php.net/xpath', 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+$xpath->registerNamespace('foo', 'urn:foo');
+
+echo "--- Legit cases: global function callable ---\n";
+
+$xpath->registerPhpFunctionNS('urn:foo', 'strtolower', strtolower(...));
+var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));
+
+echo "--- Legit cases: string callable ---\n";
+
+$xpath->registerPhpFunctionNS('urn:foo', 'strtolower', 'strtolower');
+var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));
+
+echo "--- Legit cases: trampoline callable ---\n";
+
+$xpath->registerPhpFunctionNS('urn:foo', 'test', TrampolineClass::test(...));
+var_dump($xpath->query('//a[foo:test(string(@href)) = "https://php.net"]'));
+
+echo "--- Legit cases: instance class method callable ---\n";
+
+$state = new StatefulClass;
+$xpath->registerPhpFunctionNS('urn:foo', 'test', $state->test(...));
+var_dump($xpath->query('//a[foo:test(string(@href))]'));
+var_dump($state->state);
+
+echo "--- Legit cases: global function callable that returns nothing ---\n";
+
+$xpath->registerPhpFunctionNS('urn:foo', 'test', var_dump(...));
+$xpath->query('//a[foo:test(string(@href))]');
+
+echo "--- Legit cases: multiple namespaces ---\n";
+
+$xpath->registerNamespace('bar', 'urn:bar');
+$xpath->registerPhpFunctionNS('urn:bar', 'test', 'strtolower');
+var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
+
+?>
+--EXPECT--
+--- Error cases ---
+DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xpath" because it is reserved for PHP
+DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must be a valid callback name
+DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must not contain any null bytes
+DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
+--- Legit cases: global function callable ---
+object(DOMNodeList)#7 (1) {
+ ["length"]=>
+ int(1)
+}
+--- Legit cases: string callable ---
+object(DOMNodeList)#7 (1) {
+ ["length"]=>
+ int(1)
+}
+--- Legit cases: trampoline callable ---
+string(4) "test"
+array(1) {
+ [0]=>
+ string(15) "https://PHP.net"
+}
+object(DOMNodeList)#3 (1) {
+ ["length"]=>
+ int(0)
+}
+--- Legit cases: instance class method callable ---
+object(DOMNodeList)#8 (1) {
+ ["length"]=>
+ int(1)
+}
+array(1) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(4) "test"
+ [1]=>
+ string(15) "https://PHP.net"
+ }
+}
+--- Legit cases: global function callable that returns nothing ---
+string(15) "https://PHP.net"
+--- Legit cases: multiple namespaces ---
+object(DOMNodeList)#7 (1) {
+ ["length"]=>
+ int(1)
+}
diff --git a/ext/dom/tests/registerPhpFunctionsNS.phpt b/ext/dom/tests/registerPhpFunctionsNS.phpt
deleted file mode 100644
index d05a430d2b753..0000000000000
--- a/ext/dom/tests/registerPhpFunctionsNS.phpt
+++ /dev/null
@@ -1,91 +0,0 @@
---TEST--
-registerPhpFunctionsNS() function
---EXTENSIONS--
-dom
---FILE--
-loadHTML('hello');
-
-$xpath = new DOMXPath($doc);
-
-echo "--- Error cases ---\n";
-
-try {
- $xpath->registerPhpFunctionsNS('http://php.net/xpath', ['strtolower']);
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionsNS("\0", ['strtolower']);
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
-} catch (Error $e) {
- echo $e->getMessage(), "\n";
-}
-
-echo "--- Legit cases: string callable ---\n";
-
-$xpath->registerNamespace('foo', 'urn:foo');
-$xpath->registerPhpFunctionsNS('urn:foo', 'strtolower');
-var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));
-
-echo "--- Legit cases: string callable in array ---\n";
-
-$xpath->registerPhpFunctionsNS('urn:foo', ['strtoupper']);
-var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]'));
-
-echo "--- Legit cases: callable in array ---\n";
-
-$xpath->registerPhpFunctionsNS('urn:foo', ['test' => var_dump(...)]);
-$xpath->query('//a[foo:test(string(@href))]');
-
-echo "--- Legit cases: multiple namespaces ---\n";
-
-$xpath->registerNamespace('bar', 'urn:bar');
-$xpath->registerPhpFunctionsNS('urn:bar', ['test' => 'strtolower']);
-var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
-
-?>
---EXPECT--
---- Error cases ---
-DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xpath" because it is reserved for PHP
-DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
-DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
-DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
-Object of class Closure could not be converted to string
---- Legit cases: string callable ---
-object(DOMNodeList)#6 (1) {
- ["length"]=>
- int(1)
-}
---- Legit cases: string callable in array ---
-object(DOMNodeList)#6 (1) {
- ["length"]=>
- int(0)
-}
---- Legit cases: callable in array ---
-string(15) "https://PHP.net"
---- Legit cases: multiple namespaces ---
-object(DOMNodeList)#7 (1) {
- ["length"]=>
- int(1)
-}
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index b88969c00d7ab..1652672ca57c2 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -415,17 +415,18 @@ static void dom_xpath_register_func_in_ctx(void *ctxt, const zend_string *ns, co
xmlXPathRegisterFuncNS((xmlXPathContextPtr) ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
}
-PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
+PHP_METHOD(DOMXPath, registerPhpFunctionNS)
{
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
- zend_string *namespace;
- zend_string *callable_name;
- HashTable *callable_ht;
+ zend_string *namespace, *name;
+ zend_fcall_info fci;
+ zend_fcall_info_cache fcc;
- ZEND_PARSE_PARAMETERS_START(2, 2)
+ ZEND_PARSE_PARAMETERS_START(3, 3)
Z_PARAM_PATH_STR(namespace)
- Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
+ Z_PARAM_PATH_STR(name)
+ Z_PARAM_FUNC_NO_TRAMPOLINE_FREE(fci, fcc)
ZEND_PARSE_PARAMETERS_END();
if (zend_string_equals_literal(namespace, "http://php.net/xpath")) {
@@ -433,12 +434,12 @@ PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
RETURN_THROWS();
}
- php_dom_xpath_callbacks_update_method_handler(
+ php_dom_xpath_callbacks_update_single_method_handler(
&intern->xpath_callbacks,
intern->dom.ptr,
namespace,
- callable_name,
- callable_ht,
+ name,
+ &fcc,
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
dom_xpath_register_func_in_ctx
);
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 8b539f98d4a74..273cb7630b49b 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -146,10 +146,14 @@ static bool php_dom_xpath_is_callback_name_valid(const zend_string *name, php_do
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)
+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)) {
- zend_argument_value_error(1, "must be an array containing valid callback names");
+ 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;
@@ -190,7 +194,7 @@ static zend_result php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath
if (!key) {
zend_string *str = zval_try_get_string(entry);
- if (str && php_dom_xpath_is_callback_name_valid_and_throw(str, name_validation)) {
+ if (str && php_dom_xpath_is_callback_name_valid_and_throw(str, name_validation, true)) {
zend_hash_update(&ns->functions, str, ®istered_value);
if (register_func) {
register_func(ctxt, namespace, str);
@@ -202,7 +206,7 @@ static zend_result php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath
return FAILURE;
}
} else {
- if (!php_dom_xpath_is_callback_name_valid_and_throw(key, name_validation)) {
+ if (!php_dom_xpath_is_callback_name_valid_and_throw(key, name_validation, true)) {
zend_fcc_dtor(fcc);
efree(fcc);
return FAILURE;
@@ -243,14 +247,14 @@ static zend_result php_dom_xpath_callback_ns_update_method_handler(php_dom_xpath
return SUCCESS;
}
-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)
+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(php_dom_xpath_callback_ns));
php_dom_xpath_callback_ns_ctor(registry->php_ns);
}
- return php_dom_xpath_callback_ns_update_method_handler(registry->php_ns, ctxt, ns, name, callable_ht, name_validation, register_func);
+ return registry->php_ns;
} else {
if (!registry->namespaces) {
/* In most cases probably only a single namespace is registered. */
@@ -262,10 +266,39 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_method_handler(php_dom
php_dom_xpath_callback_ns_ctor(namespace);
zend_hash_add_new_ptr(registry->namespaces, ns, namespace);
}
- return php_dom_xpath_callback_ns_update_method_handler(namespace, ctxt, ns, name, callable_ht, name_validation, register_func);
+ 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(zend_fcall_info));
+ zend_fcc_dup(allocated_fcc, fcc);
+
+ zval registered_value;
+ ZVAL_PTR(®istered_value, allocated_fcc);
+
+ zend_hash_update(&namespace->functions, name, ®istered_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) {
diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h
index 332c8c7cd56f4..5691c03fa0cab 100644
--- a/ext/dom/xpath_callbacks.h
+++ b/ext/dom/xpath_callbacks.h
@@ -60,6 +60,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *regi
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);
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);
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_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);
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);
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);
diff --git a/ext/xsl/php_xsl.stub.php b/ext/xsl/php_xsl.stub.php
index ac10e2e48f8cb..16ffaf5b31a4a 100644
--- a/ext/xsl/php_xsl.stub.php
+++ b/ext/xsl/php_xsl.stub.php
@@ -114,7 +114,7 @@ public function hasExsltSupport(): bool {}
/** @tentative-return-type */
public function registerPHPFunctions(array|string|null $functions = null): void {}
- public function registerPHPFunctionsNS(string $namespace, array|string $functions): void {}
+ public function registerPHPFunctionNS(string $namespaceURI, string $name, callable $callable): void {}
/** @return true */
public function setProfiling(?string $filename) {} // TODO make return type void
diff --git a/ext/xsl/php_xsl_arginfo.h b/ext/xsl/php_xsl_arginfo.h
index 893de080c6dab..8c9ea11924959 100644
--- a/ext/xsl/php_xsl_arginfo.h
+++ b/ext/xsl/php_xsl_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 61172e9a28769cacef9f43181cbeafdb42a87cf2 */
+ * Stub hash: 0d12e04d92a3f0cc70179814aab0491d1d3fd2f7 */
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_importStylesheet, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, stylesheet, IS_OBJECT, 0)
@@ -42,9 +42,10 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_re
ZEND_ARG_TYPE_MASK(0, functions, MAY_BE_ARRAY|MAY_BE_STRING|MAY_BE_NULL, "null")
ZEND_END_ARG_INFO()
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_registerPHPFunctionsNS, 0, 2, IS_VOID, 0)
- ZEND_ARG_TYPE_INFO(0, namespace, IS_STRING, 0)
- ZEND_ARG_TYPE_MASK(0, functions, MAY_BE_ARRAY|MAY_BE_STRING, NULL)
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_XSLTProcessor_registerPHPFunctionNS, 0, 3, IS_VOID, 0)
+ ZEND_ARG_TYPE_INFO(0, namespaceURI, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, callable, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_XSLTProcessor_setProfiling, 0, 0, 1)
@@ -68,7 +69,7 @@ ZEND_METHOD(XSLTProcessor, getParameter);
ZEND_METHOD(XSLTProcessor, removeParameter);
ZEND_METHOD(XSLTProcessor, hasExsltSupport);
ZEND_METHOD(XSLTProcessor, registerPHPFunctions);
-ZEND_METHOD(XSLTProcessor, registerPHPFunctionsNS);
+ZEND_METHOD(XSLTProcessor, registerPHPFunctionNS);
ZEND_METHOD(XSLTProcessor, setProfiling);
ZEND_METHOD(XSLTProcessor, setSecurityPrefs);
ZEND_METHOD(XSLTProcessor, getSecurityPrefs);
@@ -84,7 +85,7 @@ static const zend_function_entry class_XSLTProcessor_methods[] = {
ZEND_ME(XSLTProcessor, removeParameter, arginfo_class_XSLTProcessor_removeParameter, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, hasExsltSupport, arginfo_class_XSLTProcessor_hasExsltSupport, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, registerPHPFunctions, arginfo_class_XSLTProcessor_registerPHPFunctions, ZEND_ACC_PUBLIC)
- ZEND_ME(XSLTProcessor, registerPHPFunctionsNS, arginfo_class_XSLTProcessor_registerPHPFunctionsNS, ZEND_ACC_PUBLIC)
+ ZEND_ME(XSLTProcessor, registerPHPFunctionNS, arginfo_class_XSLTProcessor_registerPHPFunctionNS, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, setProfiling, arginfo_class_XSLTProcessor_setProfiling, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, setSecurityPrefs, arginfo_class_XSLTProcessor_setSecurityPrefs, ZEND_ACC_PUBLIC)
ZEND_ME(XSLTProcessor, getSecurityPrefs, arginfo_class_XSLTProcessor_getSecurityPrefs, ZEND_ACC_PUBLIC)
diff --git a/ext/xsl/tests/registerPHPFunctionsNS.phpt b/ext/xsl/tests/registerPHPFunctionsNS.phpt
index 384f0a16936d5..6df6da2864ef6 100644
--- a/ext/xsl/tests/registerPHPFunctionsNS.phpt
+++ b/ext/xsl/tests/registerPHPFunctionsNS.phpt
@@ -1,10 +1,26 @@
--TEST--
-registerPHPFunctionsNS() function
+registerPHPFunctionNS() function
--EXTENSIONS--
xsl
--FILE--
state[] = [$name, $arguments[0]];
+ return $arguments[0];
+ }
+}
+
function createProcessor($inputs) {
$xsl = new DomDocument();
$xsl->loadXML('
@@ -34,63 +50,65 @@ try {
echo $e->getMessage(), "\n";
}
-echo "--- Legit cases: string callable ---\n";
+echo "--- Legit cases: global function callable ---\n";
$proc = createProcessor(["foo:var_dump(string(@href))"]);
-$proc->registerPHPFunctionsNS('urn:foo', 'var_dump');
+$proc->registerPHPFunctionNS('urn:foo', 'var_dump', var_dump(...));
$proc->transformToXml($inputdom);
-echo "--- Legit cases: string callable in array ---\n";
+echo "--- Legit cases: global string callable ---\n";
$proc = createProcessor(["foo:var_dump(string(@href))"]);
-$proc->registerPHPFunctionsNS('urn:foo', ['var_dump']);
+$proc->registerPHPFunctionNS('urn:foo', 'var_dump', 'var_dump');
$proc->transformToXml($inputdom);
-echo "--- Legit cases: callable in array ---\n";
+echo "--- Legit cases: trampoline callable ---\n";
+$proc = createProcessor(["foo:trampoline(string(@href))"]);
+$proc->registerPHPFunctionNS('urn:foo', 'trampoline', TrampolineClass::test(...));
+var_dump($proc->transformToXml($inputdom));
+
+echo "--- Legit cases: instance class method callable ---\n";
+
+$state = new StatefulClass;
$proc = createProcessor(["foo:test(string(@href))"]);
-$proc->registerPHPFunctionsNS('urn:foo', ['test' => var_dump(...)]);
-$proc->transformToXml($inputdom);
+$proc->registerPHPFunctionNS('urn:foo', 'test', $state->test(...));
+var_dump($proc->transformToXml($inputdom));
+var_dump($state->state);
echo "--- Legit cases: multiple namespaces ---\n";
$proc = createProcessor(["foo:test(string(@href))", "bar:test(string(@href))"]);
-$proc->registerPHPFunctionsNS('urn:foo', ['test' => strrev(...)]);
-$proc->registerPHPFunctionsNS('urn:bar', ['test' => strtoupper(...)]);
+$proc->registerPHPFunctionNS('urn:foo', 'test', strrev(...));
+$proc->registerPHPFunctionNS('urn:bar', 'test', strtoupper(...));
var_dump($proc->transformToXml($inputdom));
echo "--- Error cases ---\n";
try {
- createProcessor([])->registerPhpFunctionsNS('http://php.net/xsl', ['strtolower']);
+ createProcessor([])->registerPhpFunctionNS('http://php.net/xsl', 'strtolower', strtolower(...));
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
try {
- createProcessor([])->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
+ createProcessor([])->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
try {
- createProcessor([])->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
+ createProcessor([])->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
try {
- createProcessor([])->registerPhpFunctionsNS("\0", ['strtolower']);
+ createProcessor([])->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
-try {
- createProcessor([])->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
-} catch (Error $e) {
- echo $e->getMessage(), "\n";
-}
-
?>
--EXPECTF--
--- Legit cases: none ---
@@ -102,19 +120,38 @@ Warning: XSLTProcessor::transformToXml(): Unregistered function in %s on line %d
Warning: XSLTProcessor::transformToXml(): runtime error: file %s line 6 element value-of in %s on line %d
Warning: XSLTProcessor::transformToXml(): XPath evaluation returned no result. in %s on line %d
---- Legit cases: string callable ---
+--- Legit cases: global function callable ---
string(15) "https://php.net"
---- Legit cases: string callable in array ---
-string(15) "https://php.net"
---- Legit cases: callable in array ---
+--- Legit cases: global string callable ---
string(15) "https://php.net"
+--- Legit cases: trampoline callable ---
+string(4) "test"
+array(1) {
+ [0]=>
+ string(15) "https://php.net"
+}
+string(26) "
+foo
+"
+--- Legit cases: instance class method callable ---
+string(38) "
+https://php.net
+"
+array(1) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(4) "test"
+ [1]=>
+ string(15) "https://php.net"
+ }
+}
--- Legit cases: multiple namespaces ---
string(53) "
ten.php//:sptthHTTPS://PHP.NET
"
--- Error cases ---
-XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xsl" because it is reserved for PHP
-XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
-XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
-XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
-Object of class Closure could not be converted to string
+XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xsl" because it is reserved for PHP
+XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must be a valid callback name
+XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must not contain any null bytes
+XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 3c7f49a4e887c..9be1fc0461d3e 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -612,17 +612,18 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctions)
}
/* }}} end XSLTProcessor::registerPHPFunctions(); */
-PHP_METHOD(XSLTProcessor, registerPHPFunctionsNS)
+PHP_METHOD(XSLTProcessor, registerPHPFunctionNS)
{
xsl_object *intern = Z_XSL_P(ZEND_THIS);
- zend_string *namespace;
- zend_string *callable_name;
- HashTable *callable_ht;
+ zend_string *namespace, *name;
+ zend_fcall_info fci;
+ zend_fcall_info_cache fcc;
- ZEND_PARSE_PARAMETERS_START(2, 2)
+ ZEND_PARSE_PARAMETERS_START(3, 3)
Z_PARAM_PATH_STR(namespace)
- Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
+ Z_PARAM_PATH_STR(name)
+ Z_PARAM_FUNC_NO_TRAMPOLINE_FREE(fci, fcc)
ZEND_PARSE_PARAMETERS_END();
if (zend_string_equals_literal(namespace, "http://php.net/xsl")) {
@@ -630,12 +631,12 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctionsNS)
RETURN_THROWS();
}
- php_dom_xpath_callbacks_update_method_handler(
+ php_dom_xpath_callbacks_update_single_method_handler(
&intern->xpath_callbacks,
NULL,
namespace,
- callable_name,
- callable_ht,
+ name,
+ &fcc,
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
NULL
);
From 48bcd446a00e0c4c7caacc93531742397979f8ed Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Mon, 1 Jan 2024 22:13:47 +0100
Subject: [PATCH 06/12] Add additional test
---
...tprocessor_exsl_registerPhpFunctionNs.phpt | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 ext/xsl/tests/xsltprocessor_exsl_registerPhpFunctionNs.phpt
diff --git a/ext/xsl/tests/xsltprocessor_exsl_registerPhpFunctionNs.phpt b/ext/xsl/tests/xsltprocessor_exsl_registerPhpFunctionNs.phpt
new file mode 100644
index 0000000000000..3d6d231c9ed0a
--- /dev/null
+++ b/ext/xsl/tests/xsltprocessor_exsl_registerPhpFunctionNs.phpt
@@ -0,0 +1,62 @@
+--TEST--
+Overriding an EXSLT builtin
+--EXTENSIONS--
+xsl
+--SKIPIF--
+hasExsltSupport()) die('skip EXSLT support not available');
+if (LIBXSLT_VERSION < 10130) die('skip too old libxsl');
+?>
+--FILE--
+textContent);
+ return 'dummy value';
+}
+
+function dummy_exit($input) {
+ var_dump($input);
+ exit("dummy exit");
+}
+
+$xsl = <<
+
+
+
+
+XML;
+
+$xml = <<
+
+XML;
+
+$xsldoc = new DOMDocument();
+$xsldoc->loadXML($xsl);
+
+$xmldoc = new DOMDocument();
+$xmldoc->loadXML($xml);
+
+$proc = new XSLTProcessor();
+$proc->importStylesheet($xsldoc);
+
+// Should override the builtin function
+$proc->registerPHPFunctionNS('http://exslt.org/dates-and-times', 'year', dummy_year(...));
+echo $proc->transformToXML($xmldoc), "\n";
+
+// Should not exit
+$proc->registerPHPFunctionNS('http://www.w3.org/1999/XSL/Transform', 'value-of', dummy_exit(...));
+echo $proc->transformToXML($xmldoc), "\n";
+
+?>
+--EXPECT--
+string(10) "2007-12-31"
+dummy value
+string(10) "2007-12-31"
+dummy value
From 28a3c40a2ca9878a7aa76500fad3a707ba85df94 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 11 Jan 2024 23:07:16 +0100
Subject: [PATCH 07/12] Split error cases off of tests
---
ext/dom/tests/DOMXPath_callables.phpt | 62 +-----------
ext/dom/tests/DOMXPath_callables_errors.phpt | 69 ++++++++++++++
ext/xsl/tests/XSLTProcessor_callables.phpt | 95 +------------------
.../tests/XSLTProcessor_callables_errors.phpt | 68 +++++++++++++
ext/xsl/tests/xpath_callables.inc | 34 +++++++
5 files changed, 174 insertions(+), 154 deletions(-)
create mode 100644 ext/dom/tests/DOMXPath_callables_errors.phpt
create mode 100644 ext/xsl/tests/XSLTProcessor_callables_errors.phpt
create mode 100644 ext/xsl/tests/xpath_callables.inc
diff --git a/ext/dom/tests/DOMXPath_callables.phpt b/ext/dom/tests/DOMXPath_callables.phpt
index d43c85158b48f..3d20527475da0 100644
--- a/ext/dom/tests/DOMXPath_callables.phpt
+++ b/ext/dom/tests/DOMXPath_callables.phpt
@@ -1,5 +1,5 @@
--TEST--
-registerPHPFunctions() with callables
+registerPHPFunctions() with callables - legit cases
--EXTENSIONS--
dom
--FILE--
@@ -70,57 +70,6 @@ echo "--- Legit cases: reset to null ---\n";
$xpath->registerPhpFunctions(null);
$xpath->evaluate("//a[php:function('var_dump', string(@href))]");
-echo "--- Error cases ---\n";
-
-$xpath = new DOMXPath($doc);
-try {
- $xpath->registerPhpFunctions("nonexistent");
-} catch (TypeError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions(function () {});
-} catch (TypeError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions([function () {}]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions([var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions(["nonexistent"]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions(["" => var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions(["\0" => var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctions("");
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
?>
--EXPECT--
--- Legit cases: none ---
@@ -137,12 +86,3 @@ No callback handler "notinset" registered
dummy: https://php.net
--- Legit cases: reset to null ---
string(15) "https://php.net"
---- Error cases ---
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a callable, function "nonexistent" not found or invalid function name
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|string|null, Closure given
-Object of class Closure could not be converted to string
-Object of class Closure could not be converted to string
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
-DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a valid callback name
diff --git a/ext/dom/tests/DOMXPath_callables_errors.phpt b/ext/dom/tests/DOMXPath_callables_errors.phpt
new file mode 100644
index 0000000000000..d11437398b158
--- /dev/null
+++ b/ext/dom/tests/DOMXPath_callables_errors.phpt
@@ -0,0 +1,69 @@
+--TEST--
+registerPHPFunctions() with callables - error cases
+--EXTENSIONS--
+dom
+--FILE--
+loadHTML('hello');
+
+$xpath = new DOMXPath($doc);
+try {
+ $xpath->registerPhpFunctions("nonexistent");
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(function () {});
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions([function () {}]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions([var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(["nonexistent"]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(["" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions(["\0" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctions("");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a callable, function "nonexistent" not found or invalid function name
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|string|null, Closure given
+Object of class Closure could not be converted to string
+Object of class Closure could not be converted to string
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
+DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a valid callback name
diff --git a/ext/xsl/tests/XSLTProcessor_callables.phpt b/ext/xsl/tests/XSLTProcessor_callables.phpt
index a845d77feec63..a7f21335e736e 100644
--- a/ext/xsl/tests/XSLTProcessor_callables.phpt
+++ b/ext/xsl/tests/XSLTProcessor_callables.phpt
@@ -1,42 +1,11 @@
--TEST--
-registerPhpFunctions() with callables
+registerPhpFunctions() with callables - legit cases
--EXTENSIONS--
xsl
--FILE--
registerPhpFunctions(["cycle" => array($this, "dummy")]);
- }
-
- public function dummy(string $var) {
- return "dummy: $var";
- }
-}
-
-function createProcessor($inputs, $class = "XSLTProcessor") {
- $xsl = new DomDocument();
- $xsl->loadXML('
-
- '
- . implode('', array_map(fn($input) => '', $inputs)) .
- '
- ');
-
- $proc = new $class();
- $proc->importStylesheet($xsl);
- return $proc;
-}
+require __DIR__ . '/xpath_callables.inc';
$inputdom = new DomDocument();
$inputdom->loadXML('hello');
@@ -80,57 +49,6 @@ $proc = createProcessor(["'cycle', string(@href)"], 'MyXSLTProcessor');
$proc->registerCycle();
var_dump($proc->transformToXml($inputdom));
-echo "--- Error cases ---\n";
-
-$proc = createProcessor([]);
-try {
- $proc->registerPhpFunctions("nonexistent");
-} catch (TypeError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions(function () {});
-} catch (TypeError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions([function () {}]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions([var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions(["nonexistent"]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions(["" => var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions(["\0" => var_dump(...)]);
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $proc->registerPhpFunctions("");
-} catch (Throwable $e) {
- echo $e->getMessage(), "\n";
-}
-
?>
--EXPECT--
--- Legit cases: none ---
@@ -153,12 +71,3 @@ No callback handler "notinset" registered
string(45) "
dummy: https://php.net
"
---- Error cases ---
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a callable, function "nonexistent" not found or invalid function name
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be of type array|string|null, Closure given
-Object of class Closure could not be converted to string
-Object of class Closure could not be converted to string
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
-XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a valid callback name
diff --git a/ext/xsl/tests/XSLTProcessor_callables_errors.phpt b/ext/xsl/tests/XSLTProcessor_callables_errors.phpt
new file mode 100644
index 0000000000000..12571f2496d02
--- /dev/null
+++ b/ext/xsl/tests/XSLTProcessor_callables_errors.phpt
@@ -0,0 +1,68 @@
+--TEST--
+registerPhpFunctions() with callables - error cases
+--EXTENSIONS--
+xsl
+--FILE--
+registerPhpFunctions("nonexistent");
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(function () {});
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions([function () {}]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions([var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(["nonexistent"]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(["" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions(["\0" => var_dump(...)]);
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $proc->registerPhpFunctions("");
+} catch (Throwable $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a callable, function "nonexistent" not found or invalid function name
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be of type array|string|null, Closure given
+Object of class Closure could not be converted to string
+Object of class Closure could not be converted to string
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be an array containing valid callback names
+XSLTProcessor::registerPHPFunctions(): Argument #1 ($functions) must be a valid callback name
diff --git a/ext/xsl/tests/xpath_callables.inc b/ext/xsl/tests/xpath_callables.inc
new file mode 100644
index 0000000000000..5db8d484eccfd
--- /dev/null
+++ b/ext/xsl/tests/xpath_callables.inc
@@ -0,0 +1,34 @@
+registerPhpFunctions(["cycle" => array($this, "dummy")]);
+ }
+
+ public function dummy(string $var) {
+ return "dummy: $var";
+ }
+}
+
+function createProcessor($inputs, $class = "XSLTProcessor") {
+ $xsl = new DomDocument();
+ $xsl->loadXML('
+
+ '
+ . implode('', array_map(fn($input) => '', $inputs)) .
+ '
+ ');
+
+ $proc = new $class();
+ $proc->importStylesheet($xsl);
+ return $proc;
+}
From 552dde09d2a7a6cd0521911a0e61d9b7fbc45fb0 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 11 Jan 2024 23:14:35 +0100
Subject: [PATCH 08/12] Address code review comments
---
ext/dom/tests/registerPhpFunctionNS.phpt | 2 +-
ext/dom/xpath.c | 25 +++++++++--------
ext/dom/xpath_callbacks.c | 16 ++++++++---
ext/xsl/tests/registerPHPFunctionsNS.phpt | 2 +-
ext/xsl/xsltprocessor.c | 34 +++++++++++------------
5 files changed, 44 insertions(+), 35 deletions(-)
diff --git a/ext/dom/tests/registerPhpFunctionNS.phpt b/ext/dom/tests/registerPhpFunctionNS.phpt
index 72410c1d0db51..c6087cc6a86ce 100644
--- a/ext/dom/tests/registerPhpFunctionNS.phpt
+++ b/ext/dom/tests/registerPhpFunctionNS.phpt
@@ -90,7 +90,7 @@ var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
?>
--EXPECT--
--- Error cases ---
-DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xpath" because it is reserved for PHP
+DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xpath" because it is reserved by PHP
DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must be a valid callback name
DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must not contain any null bytes
DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index 1652672ca57c2..272541c61a9c1 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -54,7 +54,7 @@ HashTable *dom_xpath_get_gc(zend_object *object, zval **table, int *n)
static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern, xmlXPathParserContextPtr ctxt)
{
- (void) ctxt;
+ ZEND_IGNORE_VALUE(ctxt);
ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);
@@ -63,19 +63,20 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in
static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
{
- if (!zend_is_executing()) {
+ if (UNEXPECTED(!zend_is_executing())) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: Function called from outside of PHP\n");
- } else {
- dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
- if (intern == NULL) {
- xmlGenericError(xmlGenericErrorContext,
- "xmlExtFunctionTest: failed to get the internal object\n");
- return NULL;
- }
- return intern;
+ return NULL;
+ }
+
+ dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
+ if (UNEXPECTED(intern == NULL)) {
+ xmlGenericError(xmlGenericErrorContext,
+ "xmlExtFunctionTest: failed to get the internal object\n");
+ return NULL;
}
- return NULL;
+
+ return intern;
}
static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
@@ -430,7 +431,7 @@ PHP_METHOD(DOMXPath, registerPhpFunctionNS)
ZEND_PARSE_PARAMETERS_END();
if (zend_string_equals_literal(namespace, "http://php.net/xpath")) {
- zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved for PHP");
+ zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved by PHP");
RETURN_THROWS();
}
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 273cb7630b49b..ced6cbc18a38e 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -173,7 +173,15 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_delayed_lib_registration(const php_d
}
}
-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)
+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;
@@ -392,9 +400,9 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat
fci.named_params = NULL;
ZVAL_STRINGL(&fci.function_name, function_name, function_name_length);
- zend_result result = zend_call_function(&fci, NULL);
+ zend_call_function(&fci, NULL);
zend_string_release_ex(Z_STR(fci.function_name), false);
- if (UNEXPECTED(result == FAILURE)) {
+ if (UNEXPECTED(EG(exception))) {
return FAILURE;
}
} else {
@@ -452,7 +460,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_cal
/* Last element of the stack is the function name */
xmlXPathObjectPtr obj = valuePop(ctxt);
- if (obj->stringval == NULL) {
+ if (UNEXPECTED(obj->stringval == NULL)) {
zend_type_error("Handler name must be a string");
goto cleanup;
}
diff --git a/ext/xsl/tests/registerPHPFunctionsNS.phpt b/ext/xsl/tests/registerPHPFunctionsNS.phpt
index 6df6da2864ef6..a5c82b7cf22ad 100644
--- a/ext/xsl/tests/registerPHPFunctionsNS.phpt
+++ b/ext/xsl/tests/registerPHPFunctionsNS.phpt
@@ -151,7 +151,7 @@ string(53) "
ten.php//:sptthHTTPS://PHP.NET
"
--- Error cases ---
-XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xsl" because it is reserved for PHP
+XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xsl" because it is reserved by PHP
XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must be a valid callback name
XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must not contain any null bytes
XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 9be1fc0461d3e..ef53c6d24f050 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -67,26 +67,26 @@ static void xsl_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern,
static xsl_object *xsl_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
{
- if (!zend_is_executing()) {
+ if (UNEXPECTED(!zend_is_executing())) {
xsltGenericError(xsltGenericErrorContext,
"xsltExtFunctionTest: Function called from outside of PHP\n");
- } else {
- xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt);
- if (tctxt == NULL) {
- xsltGenericError(xsltGenericErrorContext,
- "xsltExtFunctionTest: failed to get the transformation context\n");
- } else {
- xsl_object *intern = (xsl_object *) tctxt->_private;
- if (intern == NULL) {
- xsltGenericError(xsltGenericErrorContext,
- "xsltExtFunctionTest: failed to get the internal object\n");
- return NULL;
- }
- return intern;
- }
+ return NULL;
+ }
+
+ xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt);
+ if (UNEXPECTED(tctxt == NULL)) {
+ xsltGenericError(xsltGenericErrorContext,
+ "xsltExtFunctionTest: failed to get the transformation context\n");
+ return NULL;
}
- return NULL;
+ xsl_object *intern = (xsl_object *) tctxt->_private;
+ if (UNEXPECTED(intern == NULL)) {
+ xsltGenericError(xsltGenericErrorContext,
+ "xsltExtFunctionTest: failed to get the internal object\n");
+ return NULL;
+ }
+ return intern;
}
static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
@@ -627,7 +627,7 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctionNS)
ZEND_PARSE_PARAMETERS_END();
if (zend_string_equals_literal(namespace, "http://php.net/xsl")) {
- zend_argument_value_error(1, "must not be \"http://php.net/xsl\" because it is reserved for PHP");
+ zend_argument_value_error(1, "must not be \"http://php.net/xsl\" because it is reserved by PHP");
RETURN_THROWS();
}
From 32a62708ec5bd30d719883fbe3da748a465e31bc Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 11 Jan 2024 23:25:29 +0100
Subject: [PATCH 09/12] Split off more tests
---
ext/dom/tests/registerPhpFunctionNS.phpt | 41 +++---------------
.../tests/registerPhpFunctionNS_errors.phpt | 42 +++++++++++++++++++
...ionsNS.phpt => registerPHPFunctionNS.phpt} | 33 +--------------
.../tests/registerPHPFunctionNS_errors.phpt | 39 +++++++++++++++++
4 files changed, 87 insertions(+), 68 deletions(-)
create mode 100644 ext/dom/tests/registerPhpFunctionNS_errors.phpt
rename ext/xsl/tests/{registerPHPFunctionsNS.phpt => registerPHPFunctionNS.phpt} (76%)
create mode 100644 ext/xsl/tests/registerPHPFunctionNS_errors.phpt
diff --git a/ext/dom/tests/registerPhpFunctionNS.phpt b/ext/dom/tests/registerPhpFunctionNS.phpt
index c6087cc6a86ce..4c4fb157000bf 100644
--- a/ext/dom/tests/registerPhpFunctionNS.phpt
+++ b/ext/dom/tests/registerPhpFunctionNS.phpt
@@ -1,5 +1,5 @@
--TEST--
-registerPhpFunctionNS() function
+registerPhpFunctionNS() function - legit cases
--EXTENSIONS--
dom
--FILE--
@@ -26,32 +26,6 @@ $doc->loadHTML('hello');
$xpath = new DOMXPath($doc);
-echo "--- Error cases ---\n";
-
-try {
- $xpath->registerPhpFunctionNS('http://php.net/xpath', 'strtolower', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- $xpath->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
$xpath->registerNamespace('foo', 'urn:foo');
echo "--- Legit cases: global function callable ---\n";
@@ -89,18 +63,13 @@ var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
?>
--EXPECT--
---- Error cases ---
-DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xpath" because it is reserved by PHP
-DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must be a valid callback name
-DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must not contain any null bytes
-DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
--- Legit cases: global function callable ---
-object(DOMNodeList)#7 (1) {
+object(DOMNodeList)#5 (1) {
["length"]=>
int(1)
}
--- Legit cases: string callable ---
-object(DOMNodeList)#7 (1) {
+object(DOMNodeList)#5 (1) {
["length"]=>
int(1)
}
@@ -115,7 +84,7 @@ object(DOMNodeList)#3 (1) {
int(0)
}
--- Legit cases: instance class method callable ---
-object(DOMNodeList)#8 (1) {
+object(DOMNodeList)#6 (1) {
["length"]=>
int(1)
}
@@ -131,7 +100,7 @@ array(1) {
--- Legit cases: global function callable that returns nothing ---
string(15) "https://PHP.net"
--- Legit cases: multiple namespaces ---
-object(DOMNodeList)#7 (1) {
+object(DOMNodeList)#5 (1) {
["length"]=>
int(1)
}
diff --git a/ext/dom/tests/registerPhpFunctionNS_errors.phpt b/ext/dom/tests/registerPhpFunctionNS_errors.phpt
new file mode 100644
index 0000000000000..985376daee914
--- /dev/null
+++ b/ext/dom/tests/registerPhpFunctionNS_errors.phpt
@@ -0,0 +1,42 @@
+--TEST--
+registerPhpFunctionNS() function - error cases
+--EXTENSIONS--
+dom
+--FILE--
+loadHTML('hello');
+
+$xpath = new DOMXPath($doc);
+
+try {
+ $xpath->registerPhpFunctionNS('http://php.net/xpath', 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ $xpath->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xpath" because it is reserved by PHP
+DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must be a valid callback name
+DOMXPath::registerPhpFunctionNS(): Argument #2 ($name) must not contain any null bytes
+DOMXPath::registerPhpFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
diff --git a/ext/xsl/tests/registerPHPFunctionsNS.phpt b/ext/xsl/tests/registerPHPFunctionNS.phpt
similarity index 76%
rename from ext/xsl/tests/registerPHPFunctionsNS.phpt
rename to ext/xsl/tests/registerPHPFunctionNS.phpt
index a5c82b7cf22ad..880a8f1aae06e 100644
--- a/ext/xsl/tests/registerPHPFunctionsNS.phpt
+++ b/ext/xsl/tests/registerPHPFunctionNS.phpt
@@ -1,5 +1,5 @@
--TEST--
-registerPHPFunctionNS() function
+registerPHPFunctionNS() function - legit cases
--EXTENSIONS--
xsl
--FILE--
@@ -83,32 +83,6 @@ $proc->registerPHPFunctionNS('urn:foo', 'test', strrev(...));
$proc->registerPHPFunctionNS('urn:bar', 'test', strtoupper(...));
var_dump($proc->transformToXml($inputdom));
-echo "--- Error cases ---\n";
-
-try {
- createProcessor([])->registerPhpFunctionNS('http://php.net/xsl', 'strtolower', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- createProcessor([])->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- createProcessor([])->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
-try {
- createProcessor([])->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
-} catch (ValueError $e) {
- echo $e->getMessage(), "\n";
-}
-
?>
--EXPECTF--
--- Legit cases: none ---
@@ -150,8 +124,3 @@ array(1) {
string(53) "
ten.php//:sptthHTTPS://PHP.NET
"
---- Error cases ---
-XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xsl" because it is reserved by PHP
-XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must be a valid callback name
-XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must not contain any null bytes
-XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
diff --git a/ext/xsl/tests/registerPHPFunctionNS_errors.phpt b/ext/xsl/tests/registerPHPFunctionNS_errors.phpt
new file mode 100644
index 0000000000000..272e6d00adb72
--- /dev/null
+++ b/ext/xsl/tests/registerPHPFunctionNS_errors.phpt
@@ -0,0 +1,39 @@
+--TEST--
+registerPHPFunctionNS() function - error cases
+--EXTENSIONS--
+xsl
+--FILE--
+registerPhpFunctionNS('http://php.net/xsl', 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionNS('urn:foo', 'x:a', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionNS("urn:foo", "\0", strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ createProcessor([])->registerPhpFunctionNS("\0", 'strtolower', strtolower(...));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not be "http://php.net/xsl" because it is reserved by PHP
+XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must be a valid callback name
+XSLTProcessor::registerPHPFunctionNS(): Argument #2 ($name) must not contain any null bytes
+XSLTProcessor::registerPHPFunctionNS(): Argument #1 ($namespaceURI) must not contain any null bytes
From eb1e44956234afbef02f79b548a785284f67e20b Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Fri, 12 Jan 2024 23:53:49 +0100
Subject: [PATCH 10/12] [ci skip] NEWS
---
NEWS | 2 ++
1 file changed, 2 insertions(+)
diff --git a/NEWS b/NEWS
index 0d731289597ad..b239e8da75735 100644
--- a/NEWS
+++ b/NEWS
@@ -19,6 +19,7 @@ DOM:
. Implement DOM HTML5 parsing and serialization RFC. (nielsdos)
. Fix DOMElement->prefix with empty string creates bogus prefix. (nielsdos)
. Handle OOM more consistently. (nielsdos)
+ . Implemented "Improve callbacks in ext/dom and ext/xsl" RFC. (nielsdos)
FPM:
. Implement GH-12385 (flush headers without body when calling flush()).
@@ -130,5 +131,6 @@ XML:
XSL:
. Implement request #64137 (XSLTProcessor::setParameter() should allow both
quotes to be used). (nielsdos)
+ . Implemented "Improve callbacks in ext/dom and ext/xsl" RFC. (nielsdos)
<<< NOTE: Insert NEWS from last stable release here prior to actual release! >>>
From 657679e5f55d6c4ace06611477f64e6dacd7e3e2 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Fri, 12 Jan 2024 23:59:06 +0100
Subject: [PATCH 11/12] [ci skip] UPGRADING
---
UPGRADING | 13 +++++++++++++
UPGRADING.INTERNALS | 2 ++
2 files changed, 15 insertions(+)
diff --git a/UPGRADING b/UPGRADING
index 7ccc9751c0d09..ee6fec4429dd2 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -113,6 +113,9 @@ PHP 8.4 UPGRADE NOTES
. XSLTProcessor::setParameter() will now throw a ValueError when its arguments
contain null bytes. This never actually worked correctly in the first place,
which is why it throws an exception nowadays.
+ . Failure to call a PHP function callback during evaluation now throw
+ instead of emitting a warning.
+ RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
========================================
2. New Features
@@ -137,6 +140,8 @@ PHP 8.4 UPGRADE NOTES
These classes provide a cleaner API to handle HTML and XML documents.
Furthermore, the DOM\HTMLDocument class implements spec-compliant HTML5
parsing and serialization.
+ . It is now possible to pass any callable to registerPhpFunctions().
+ RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
- FPM:
. Flushing headers without a body will now succeed. See GH-12785.
@@ -160,6 +165,8 @@ PHP 8.4 UPGRADE NOTES
- XSL:
. It is now possible to use parameters that contain both single and double
quotes.
+ . It is now possible to pass any callable to registerPhpFunctions().
+ RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
========================================
3. Changes in SAPI modules
@@ -279,6 +286,8 @@ PHP 8.4 UPGRADE NOTES
- DOM:
. Added DOMNode::compareDocumentPosition().
+ . Added DOMXPath::registerPhpFunctionNS().
+ RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
- MBString:
. Added mb_trim, mb_ltrim and mb_rtrim functions.
@@ -295,6 +304,10 @@ PHP 8.4 UPGRADE NOTES
. sodium_crypto_aead_aes256gcm_*() functions are now enabled on aarch64 CPUs
with the ARM cryptographic extensions.
+- XSL:
+ . Added XSLTProcessor::registerPhpFunctionNS().
+ RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl
+
========================================
7. New Classes and Interfaces
========================================
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 6f35c59983237..bb3e1f9a7664a 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -42,6 +42,8 @@ PHP 8.4 INTERNALS UPGRADE NOTES
- dom_read_t and dom_write_t now expect the function to return zend_result
instead of int.
- The macros DOM_NO_ARGS() and DOM_NOT_IMPLEMENTED() have been removed.
+ - New public APIs are available to handle callbacks from XPath, see
+ xpath_callbacks.h.
b. ext/random
- The macro RAND_RANGE_BADSCALING() has been removed. The implementation
From 95618207189038c1b92f8e0937a8b7e799a3bfd9 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Sat, 13 Jan 2024 00:00:14 +0100
Subject: [PATCH 12/12] [ci skip] UPGRADING
---
UPGRADING | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/UPGRADING b/UPGRADING
index ee6fec4429dd2..0528fa2c2e702 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -113,7 +113,7 @@ PHP 8.4 UPGRADE NOTES
. XSLTProcessor::setParameter() will now throw a ValueError when its arguments
contain null bytes. This never actually worked correctly in the first place,
which is why it throws an exception nowadays.
- . Failure to call a PHP function callback during evaluation now throw
+ . Failure to call a PHP function callback during evaluation now throws
instead of emitting a warning.
RFC: https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl