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! >>> diff --git a/UPGRADING b/UPGRADING index 7ccc9751c0d09..0528fa2c2e702 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 throws + 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 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/php_dom.stub.php b/ext/dom/php_dom.stub.php index db86fe249b8ec..bda09872694a9 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 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 3a2ec9eddc034..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: 5512165ddaad08287561abac2a325e2aab3c6188 */ + * 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) @@ -451,6 +451,14 @@ 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_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 + 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 +746,9 @@ ZEND_METHOD(DOMXPath, registerNamespace); #if defined(LIBXML_XPATH_ENABLED) ZEND_METHOD(DOMXPath, registerPhpFunctions); #endif +#if defined(LIBXML_XPATH_ENABLED) +ZEND_METHOD(DOMXPath, registerPhpFunctionNS); +#endif ZEND_METHOD(DOM_Document, createAttribute); ZEND_METHOD(DOM_Document, createAttributeNS); ZEND_METHOD(DOM_Document, createCDATASection); @@ -1014,6 +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, registerPhpFunctionNS, arginfo_class_DOMXPath_registerPhpFunctionNS, ZEND_ACC_PUBLIC) ZEND_FE_END }; #endif diff --git a/ext/dom/tests/DOMXPath_callables.phpt b/ext/dom/tests/DOMXPath_callables.phpt new file mode 100644 index 0000000000000..3d20527475da0 --- /dev/null +++ b/ext/dom/tests/DOMXPath_callables.phpt @@ -0,0 +1,88 @@ +--TEST-- +registerPHPFunctions() with callables - legit cases +--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 "--- Legit cases: reset to null ---\n"; + +$xpath->registerPhpFunctions(null); +$xpath->evaluate("//a[php:function('var_dump', string(@href))]"); + +?> +--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 +--- Legit cases: reset to null --- +string(15) "https://php.net" 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/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/tests/registerPhpFunctionNS.phpt b/ext/dom/tests/registerPhpFunctionNS.phpt new file mode 100644 index 0000000000000..4c4fb157000bf --- /dev/null +++ b/ext/dom/tests/registerPhpFunctionNS.phpt @@ -0,0 +1,106 @@ +--TEST-- +registerPhpFunctionNS() function - legit cases +--EXTENSIONS-- +dom +--FILE-- +state[] = [$name, $arguments[0]]; + return $arguments[0]; + } +} + +$doc = new DOMDocument(); +$doc->loadHTML('hello'); + +$xpath = new DOMXPath($doc); + +$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-- +--- Legit cases: global function callable --- +object(DOMNodeList)#5 (1) { + ["length"]=> + int(1) +} +--- Legit cases: string callable --- +object(DOMNodeList)#5 (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)#6 (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)#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/dom/xpath.c b/ext/dom/xpath.c index 7522ec3f1df86..272541c61a9c1 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -32,179 +32,86 @@ #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 (! zend_is_executing()) { - xmlGenericError(xmlGenericErrorContext, - "xmlExtFunctionTest: Function called from outside of PHP\n"); - error = 1; - } 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 (intern->dom.ptr != NULL) { + xmlXPathFreeContext((xmlXPathContextPtr) intern->dom.ptr); + php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom); } - if (error == 1) { - for (i = nargs - 1; i >= 0; i--) { - obj = valuePop(ctxt); - xmlXPathFreeObject(obj); - } - return; - } + php_dom_xpath_callbacks_dtor(&intern->xpath_callbacks); +} - if (UNEXPECTED(nargs == 0)) { - zend_throw_error(NULL, "Function name must be passed as the first argument"); - return; - } +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) +{ + ZEND_IGNORE_VALUE(ctxt); - fci.param_count = nargs - 1; - if (fci.param_count > 0) { - fci.params = safe_emalloc(fci.param_count, sizeof(zval), 0); + ZEND_ASSERT(node->type != XML_NAMESPACE_DECL); + + php_dom_create_object(node, child, intern); +} + +static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt) +{ + if (UNEXPECTED(!zend_is_executing())) { + xmlGenericError(xmlGenericErrorContext, + "xmlExtFunctionTest: Function called from outside of PHP\n"); + return NULL; } - /* 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)); - } - xmlXPathFreeObject(obj); + + 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; } - fci.size = sizeof(fci); + return intern; +} - /* 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; +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 { - 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_php_ns(&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); } /* }}} */ +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) { @@ -482,35 +389,62 @@ 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; + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + + 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(ht, name) + Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, callable_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; + 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(void *ctxt, const zend_string *ns, const zend_string *name) +{ + xmlXPathRegisterFuncNS((xmlXPathContextPtr) ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline); +} + +PHP_METHOD(DOMXPath, registerPhpFunctionNS) +{ + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + + zend_string *namespace, *name; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_PATH_STR(namespace) + 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")) { + zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved by PHP"); + RETURN_THROWS(); } + php_dom_xpath_callbacks_update_single_method_handler( + &intern->xpath_callbacks, + intern->dom.ptr, + namespace, + name, + &fcc, + PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME, + dom_xpath_register_func_in_ctx + ); } -/* }}} end dom_xpath_register_php_functions */ #endif /* LIBXML_XPATH_ENABLED */ diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c new file mode 100644 index 0000000000000..ced6cbc18a38e --- /dev/null +++ b/ext/dom/xpath_callbacks.c @@ -0,0 +1,512 @@ +/* + +----------------------------------------------------------------------+ + | 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_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) +{ +} + +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->php_ns) { + php_dom_xpath_callback_ns_dtor(registry->php_ns); + efree(registry->php_ns); + } + if (registry->namespaces) { + php_dom_xpath_callback_ns *ns; + ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) { + php_dom_xpath_callback_ns_dtor(ns); + efree(ns); + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(registry->namespaces); + FREE_HASHTABLE(registry->namespaces); + } + php_dom_xpath_callbacks_clean_node_list(registry); +} + +static void php_dom_xpath_callback_ns_get_gc(php_dom_xpath_callback_ns *ns, zend_get_gc_buffer *gc_buffer) +{ + zend_fcall_info_cache *entry; + ZEND_HASH_MAP_FOREACH_PTR(&ns->functions, entry) { + zend_get_gc_buffer_add_fcc(gc_buffer, entry); + } ZEND_HASH_FOREACH_END(); +} + +PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer) +{ + if (registry->php_ns) { + php_dom_xpath_callback_ns_get_gc(registry->php_ns, gc_buffer); + } + if (registry->namespaces) { + php_dom_xpath_callback_ns *ns; + ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) { + php_dom_xpath_callback_ns_get_gc(ns, gc_buffer); + } ZEND_HASH_FOREACH_END(); + } +} + +PHP_DOM_EXPORT HashTable *php_dom_xpath_callbacks_get_gc_for_whole_object(php_dom_xpath_callbacks *registry, zend_object *object, zval **table, int *n) +{ + if (registry->php_ns || registry->namespaces) { + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + php_dom_xpath_callbacks_get_gc(registry, gc_buffer); + zend_get_gc_buffer_use(gc_buffer, table, n); + + if (object->properties == NULL && object->ce->default_properties_count == 0) { + return NULL; + } else { + return zend_std_get_properties(object); + } + } else { + return zend_std_get_gc(object, table, n); + } +} + +static bool php_dom_xpath_is_callback_name_valid(const zend_string *name, php_dom_xpath_callback_name_validation name_validation) +{ + if (ZSTR_LEN(name) == 0) { + return false; + } + + if (name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS || name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME) { + if (zend_str_has_nul_byte(name)) { + return false; + } + } + + if (name_validation == PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME) { + if (xmlValidateNCName((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, bool is_array) +{ + if (!php_dom_xpath_is_callback_name_valid(name, name_validation)) { + if (is_array) { + zend_argument_value_error(1, "must be an array containing valid callback names"); + } else { + zend_argument_value_error(2, "must be a valid callback name"); + } + return false; + } + return true; +} + +PHP_DOM_EXPORT void php_dom_xpath_callbacks_delayed_lib_registration(const php_dom_xpath_callbacks* registry, void *ctxt, php_dom_xpath_callbacks_register_func_ctx register_func) +{ + if (registry->namespaces) { + zend_string *namespace; + php_dom_xpath_callback_ns *ns; + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(registry->namespaces, namespace, ns) { + zend_string *name; + ZEND_HASH_MAP_FOREACH_STR_KEY(&ns->functions, name) { + register_func(ctxt, namespace, name); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + } +} + +static zend_result php_dom_xpath_callback_ns_update_method_handler( + php_dom_xpath_callback_ns* ns, + xmlXPathContextPtr ctxt, + const zend_string *namespace, + zend_string *name, + const HashTable *callable_ht, + php_dom_xpath_callback_name_validation name_validation, + php_dom_xpath_callbacks_register_func_ctx register_func +) +{ + zval *entry, registered_value; + + if (callable_ht) { + zend_string *key; + ZEND_HASH_FOREACH_STR_KEY_VAL(callable_ht, key, entry) { + zend_fcall_info_cache* fcc = emalloc(sizeof(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 FAILURE; + } + + zend_fcc_addref(fcc); + ZVAL_PTR(®istered_value, fcc); + + if (!key) { + zend_string *str = zval_try_get_string(entry); + 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); + } + zend_string_release_ex(str, false); + } else { + zend_fcc_dtor(fcc); + efree(fcc); + return FAILURE; + } + } else { + if (!php_dom_xpath_is_callback_name_valid_and_throw(key, name_validation, true)) { + zend_fcc_dtor(fcc); + efree(fcc); + return FAILURE; + } + zend_hash_update(&ns->functions, key, ®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; + ZVAL_STR(&tmp, name); + if (!zend_is_callable_ex(&tmp, NULL, 0, NULL, fcc, &error)) { + zend_argument_type_error(1, "must be a callable, %s", error); + efree(fcc); + efree(error); + return FAILURE; + } + zend_fcc_addref(fcc); + ZVAL_PTR(®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; +} + +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 registry->php_ns; + } else { + if (!registry->namespaces) { + /* In most cases probably only a single namespace is registered. */ + registry->namespaces = zend_new_array(1); + } + php_dom_xpath_callback_ns *namespace = zend_hash_find_ptr(registry->namespaces, ns); + if (namespace == NULL) { + namespace = emalloc(sizeof(php_dom_xpath_callback_ns)); + php_dom_xpath_callback_ns_ctor(namespace); + zend_hash_add_new_ptr(registry->namespaces, ns, namespace); + } + return namespace; + } +} + +PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_method_handler(php_dom_xpath_callbacks* registry, xmlXPathContextPtr ctxt, zend_string *ns, zend_string *name, const HashTable *callable_ht, php_dom_xpath_callback_name_validation name_validation, php_dom_xpath_callbacks_register_func_ctx register_func) +{ + php_dom_xpath_callback_ns *namespace = php_dom_xpath_callbacks_ensure_ns(registry, ns); + return php_dom_xpath_callback_ns_update_method_handler(namespace, ctxt, ns, name, callable_ht, name_validation, register_func); +} + +PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_single_method_handler(php_dom_xpath_callbacks* registry, xmlXPathContextPtr ctxt, zend_string *ns, zend_string *name, const zend_fcall_info_cache *fcc, php_dom_xpath_callback_name_validation name_validation, php_dom_xpath_callbacks_register_func_ctx register_func) +{ + if (!php_dom_xpath_is_callback_name_valid_and_throw(name, name_validation, false)) { + return FAILURE; + } + + php_dom_xpath_callback_ns *namespace = php_dom_xpath_callbacks_ensure_ns(registry, ns); + zend_fcall_info_cache* allocated_fcc = emalloc(sizeof(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) { + return NULL; + } + + zval *params = safe_emalloc(param_count, sizeof(zval), 0); + + /* Reverse order to pop values off ctxt stack */ + for (zval *param = params + param_count - 1; param >= params; param--) { + xmlXPathObjectPtr obj = valuePop(ctxt); + ZEND_ASSERT(obj != NULL); + switch (obj->type) { + case XPATH_STRING: + ZVAL_STRING(param, (char *)obj->stringval); + break; + case XPATH_BOOLEAN: + ZVAL_BOOL(param, obj->boolval); + break; + case XPATH_NUMBER: + ZVAL_DOUBLE(param, obj->floatval); + break; + case XPATH_NODESET: + if (evaluation_mode == PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING) { + char *str = (char *)xmlXPathCastToString(obj); + ZVAL_STRING(param, str); + xmlFree(str); + } else if (evaluation_mode == PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET) { + if (obj->nodesetval && obj->nodesetval->nodeNr > 0) { + array_init_size(param, obj->nodesetval->nodeNr); + zend_hash_real_init_packed(Z_ARRVAL_P(param)); + for (int j = 0; j < obj->nodesetval->nodeNr; j++) { + xmlNodePtr node = obj->nodesetval->nodeTab[j]; + zval child; + if (UNEXPECTED(node->type == XML_NAMESPACE_DECL)) { + xmlNodePtr nsparent = node->_private; + xmlNsPtr original = (xmlNsPtr) node; + + /* Make sure parent dom object exists, so we can take an extra reference. */ + zval parent_zval; /* don't destroy me, my lifetime is 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); + } + 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); + } + + 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); + } +} + +static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpath_callbacks, php_dom_xpath_callback_ns *ns, xmlXPathParserContextPtr ctxt, zval *params, uint32_t param_count, const char *function_name, size_t function_name_length) +{ + zval callback_retval; + + if (UNEXPECTED(ns == NULL)) { + zend_throw_error(NULL, "No callbacks were registered"); + return FAILURE; + } + + if (ns->mode == PHP_DOM_REG_FUNC_MODE_ALL) { + zend_fcall_info fci; + fci.size = sizeof(fci); + fci.object = NULL; + fci.retval = &callback_retval; + fci.param_count = param_count; + fci.params = params; + fci.named_params = NULL; + ZVAL_STRINGL(&fci.function_name, function_name, function_name_length); + + zend_call_function(&fci, NULL); + zend_string_release_ex(Z_STR(fci.function_name), false); + if (UNEXPECTED(EG(exception))) { + return FAILURE; + } + } else { + ZEND_ASSERT(ns->mode == PHP_DOM_REG_FUNC_MODE_SET); + + zval *fcc_wrapper = zend_hash_str_find(&ns->functions, function_name, function_name_length); + if (fcc_wrapper) { + zend_call_known_fcc(Z_PTR_P(fcc_wrapper), &callback_retval, param_count, params, NULL); + } else { + zend_throw_error(NULL, "No callback handler \"%s\" registered", function_name); + return FAILURE; + } + } + + if (Z_TYPE(callback_retval) != IS_UNDEF) { + if (Z_TYPE(callback_retval) == IS_OBJECT && instanceof_function(Z_OBJCE(callback_retval), dom_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_new(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); + return FAILURE; + } 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); + } + + return SUCCESS; +} + +PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, int num_args, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory) +{ + zend_result result = FAILURE; + + if (UNEXPECTED(num_args == 0)) { + zend_throw_error(NULL, "Function name must be passed as the first argument"); + goto cleanup_no_obj; + } + + uint32_t param_count = num_args - 1; + zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory); + + /* Last element of the stack is the function name */ + xmlXPathObjectPtr obj = valuePop(ctxt); + if (UNEXPECTED(obj->stringval == NULL)) { + zend_type_error("Handler name must be a string"); + goto cleanup; + } + + const char *function_name = (const char *) obj->stringval; + size_t function_name_length = strlen(function_name); + + result = php_dom_xpath_callback_dispatch(xpath_callbacks, xpath_callbacks->php_ns, ctxt, params, param_count, function_name, function_name_length); + +cleanup: + xmlXPathFreeObject(obj); + php_dom_xpath_callback_cleanup_args(params, param_count); +cleanup_no_obj: + if (UNEXPECTED(result != SUCCESS)) { + /* Push sentinel value */ + valuePush(ctxt, xmlXPathNewString((const xmlChar *) "")); + } + + return result; +} + +PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_custom_ns(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, int num_args, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory) +{ + uint32_t param_count = num_args; + zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory); + + const char *namespace = (const char *) ctxt->context->functionURI; + /* Impossible because it wouldn't have been registered inside the context. */ + ZEND_ASSERT(xpath_callbacks->namespaces != NULL); + + php_dom_xpath_callback_ns *ns = zend_hash_str_find_ptr(xpath_callbacks->namespaces, namespace, strlen(namespace)); + /* Impossible because it wouldn't have been registered inside the context. */ + ZEND_ASSERT(ns != NULL); + + const char *function_name = (const char *) ctxt->context->function; + size_t function_name_length = strlen(function_name); + + zend_result result = php_dom_xpath_callback_dispatch(xpath_callbacks, ns, ctxt, params, param_count, function_name, function_name_length); + + php_dom_xpath_callback_cleanup_args(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 new file mode 100644 index 0000000000000..5691c03fa0cab --- /dev/null +++ b/ext/dom/xpath_callbacks.h @@ -0,0 +1,68 @@ +/* + +----------------------------------------------------------------------+ + | 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 void (*php_dom_xpath_callbacks_register_func_ctx)(void *ctxt, const zend_string *ns, const zend_string *name); + +typedef struct { + HashTable functions; + php_dom_register_functions_mode mode; +} php_dom_xpath_callback_ns; + +typedef struct { + php_dom_xpath_callback_ns *php_ns; + HashTable *namespaces; + 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_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); + +#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/php_xsl.stub.php b/ext/xsl/php_xsl.stub.php index 9394562425d78..16ffaf5b31a4a 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 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 92a7ab61e7810..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: 87ea452722956b6cfe46458e7fcd97f0bcfb767b */ + * 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,6 +42,12 @@ 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_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) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 1) ZEND_END_ARG_INFO() @@ -63,6 +69,7 @@ ZEND_METHOD(XSLTProcessor, getParameter); ZEND_METHOD(XSLTProcessor, removeParameter); ZEND_METHOD(XSLTProcessor, hasExsltSupport); ZEND_METHOD(XSLTProcessor, registerPHPFunctions); +ZEND_METHOD(XSLTProcessor, registerPHPFunctionNS); ZEND_METHOD(XSLTProcessor, setProfiling); ZEND_METHOD(XSLTProcessor, setSecurityPrefs); ZEND_METHOD(XSLTProcessor, getSecurityPrefs); @@ -78,6 +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, 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/XSLTProcessor_callables.phpt b/ext/xsl/tests/XSLTProcessor_callables.phpt new file mode 100644 index 0000000000000..a7f21335e736e --- /dev/null +++ b/ext/xsl/tests/XSLTProcessor_callables.phpt @@ -0,0 +1,73 @@ +--TEST-- +registerPhpFunctions() with callables - legit cases +--EXTENSIONS-- +xsl +--FILE-- +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)); + +?> +--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 +" 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/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/registerPHPFunctionNS.phpt b/ext/xsl/tests/registerPHPFunctionNS.phpt new file mode 100644 index 0000000000000..880a8f1aae06e --- /dev/null +++ b/ext/xsl/tests/registerPHPFunctionNS.phpt @@ -0,0 +1,126 @@ +--TEST-- +registerPHPFunctionNS() function - legit cases +--EXTENSIONS-- +xsl +--FILE-- +state[] = [$name, $arguments[0]]; + return $arguments[0]; + } +} + +function createProcessor($inputs) { + $xsl = new DomDocument(); + $xsl->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: global function callable ---\n"; + +$proc = createProcessor(["foo:var_dump(string(@href))"]); +$proc->registerPHPFunctionNS('urn:foo', 'var_dump', var_dump(...)); +$proc->transformToXml($inputdom); + +echo "--- Legit cases: global string callable ---\n"; + +$proc = createProcessor(["foo:var_dump(string(@href))"]); +$proc->registerPHPFunctionNS('urn:foo', 'var_dump', 'var_dump'); +$proc->transformToXml($inputdom); + +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->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->registerPHPFunctionNS('urn:foo', 'test', strrev(...)); +$proc->registerPHPFunctionNS('urn:bar', 'test', strtoupper(...)); +var_dump($proc->transformToXml($inputdom)); + +?> +--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: global function callable --- +string(15) "https://php.net" +--- 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 +" 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 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/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; +} 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_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 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..ef53c6d24f050 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -45,231 +45,83 @@ 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); +} - if (! zend_is_executing()) { +static xsl_object *xsl_ext_fetch_intern(xmlXPathParserContextPtr ctxt) +{ + if (UNEXPECTED(!zend_is_executing())) { xsltGenericError(xsltGenericErrorContext, "xsltExtFunctionTest: Function called from outside of PHP\n"); - error = 1; - } else { - tctxt = xsltXPathGetTransformContext(ctxt); - if (tctxt == NULL) { - xsltGenericError(xsltGenericErrorContext, - "xsltExtFunctionTest: failed to get the transformation context\n"); - error = 1; - } 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; - } - } - } - - if (error == 1) { - for (i = nargs - 1; i >= 0; i--) { - obj = valuePop(ctxt); - if (obj) { - xmlXPathFreeObject(obj); - } - } - return; + return NULL; } - if (UNEXPECTED(nargs == 0)) { - zend_throw_error(NULL, "Function name must be passed as the first argument"); - return; + xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt); + if (UNEXPECTED(tctxt == NULL)) { + xsltGenericError(xsltGenericErrorContext, + "xsltExtFunctionTest: failed to get the transformation context\n"); + return NULL; } - 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); + 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; +} - fci.size = sizeof(fci); - fci.named_params = NULL; - if (fci.param_count > 0) { - fci.params = args; +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 { - 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]); - } - 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 *) "")); - } 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_php_ns(&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); } /* }}} */ +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: */ @@ -353,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; @@ -459,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 { @@ -475,11 +335,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,40 +590,57 @@ 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; + xsl_object *intern = Z_XSL_P(ZEND_THIS); + + 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(restrict_ht, restrict_str) + Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name) ZEND_PARSE_PARAMETERS_END(); - intern = Z_XSL_P(id); + 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(); */ - 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(); +PHP_METHOD(XSLTProcessor, registerPHPFunctionNS) +{ + xsl_object *intern = Z_XSL_P(ZEND_THIS); - 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; + zend_string *namespace, *name; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_PATH_STR(namespace) + 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")) { + zend_argument_value_error(1, "must not be \"http://php.net/xsl\" because it is reserved by PHP"); + RETURN_THROWS(); } + + php_dom_xpath_callbacks_update_single_method_handler( + &intern->xpath_callbacks, + NULL, + namespace, + name, + &fcc, + PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME, + NULL + ); } -/* }}} end XSLTProcessor::registerPHPFunctions(); */ /* {{{ */ PHP_METHOD(XSLTProcessor, setProfiling)