Skip to content

Commit 70817b9

Browse files
committed
Implement namespaced functions for XSL
1 parent e8b9b5e commit 70817b9

8 files changed

+215
-23
lines changed

ext/dom/tests/registerPhpFunctionsNS.phpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]')
5555

5656
echo "--- Legit cases: callable in array ---\n";
5757

58-
$xpath->registerPhpFunctionsNS('urn:foo', ['test' => 'var_dump']);
58+
$xpath->registerPhpFunctionsNS('urn:foo', ['test' => var_dump(...)]);
5959
$xpath->query('//a[foo:test(string(@href))]');
6060

6161
echo "--- Legit cases: multiple namespaces ---\n";
@@ -85,7 +85,7 @@ object(DOMNodeList)#6 (1) {
8585
--- Legit cases: callable in array ---
8686
string(15) "https://PHP.net"
8787
--- Legit cases: multiple namespaces ---
88-
object(DOMNodeList)#4 (1) {
88+
object(DOMNodeList)#7 (1) {
8989
["length"]=>
9090
int(1)
9191
}

ext/dom/xpath.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,9 @@ PHP_METHOD(DOMXPath, registerPhpFunctions)
410410
}
411411
/* }}} end dom_xpath_register_php_functions */
412412

413-
static void dom_xpath_register_func_in_ctx(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name)
413+
static void dom_xpath_register_func_in_ctx(void *ctxt, const zend_string *ns, const zend_string *name)
414414
{
415-
xmlXPathRegisterFuncNS(ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
415+
xmlXPathRegisterFuncNS((xmlXPathContextPtr) ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
416416
}
417417

418418
PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
@@ -428,7 +428,7 @@ PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
428428
Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
429429
ZEND_PARSE_PARAMETERS_END();
430430

431-
if (zend_string_equals_literal(namespace, "http://php.net/xpath")) { // TODO: this is different for XSL!!!
431+
if (zend_string_equals_literal(namespace, "http://php.net/xpath")) {
432432
zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved for PHP");
433433
RETURN_THROWS();
434434
}

ext/dom/xpath_callbacks.c

+19-6
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *regist
7676
}
7777
if (registry->namespaces) {
7878
php_dom_xpath_callback_ns *ns;
79-
ZEND_HASH_FOREACH_PTR(registry->namespaces, ns) {
79+
ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
8080
php_dom_xpath_callback_ns_dtor(ns);
8181
efree(ns);
8282
} ZEND_HASH_FOREACH_END();
@@ -89,10 +89,9 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *regist
8989

9090
static void php_dom_xpath_callback_ns_get_gc(php_dom_xpath_callback_ns *ns, zend_get_gc_buffer *gc_buffer)
9191
{
92-
zval *entry;
93-
ZEND_HASH_FOREACH_VAL(&ns->functions, entry) {
94-
ZEND_ASSERT(Z_TYPE_P(entry) == IS_PTR);
95-
zend_get_gc_buffer_add_fcc(gc_buffer, Z_PTR_P(entry));
92+
zend_fcall_info_cache *entry;
93+
ZEND_HASH_MAP_FOREACH_PTR(&ns->functions, entry) {
94+
zend_get_gc_buffer_add_fcc(gc_buffer, entry);
9695
} ZEND_HASH_FOREACH_END();
9796
}
9897

@@ -103,7 +102,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *regi
103102
}
104103
if (registry->namespaces) {
105104
php_dom_xpath_callback_ns *ns;
106-
ZEND_HASH_FOREACH_PTR(registry->namespaces, ns) {
105+
ZEND_HASH_MAP_FOREACH_PTR(registry->namespaces, ns) {
107106
php_dom_xpath_callback_ns_get_gc(ns, gc_buffer);
108107
} ZEND_HASH_FOREACH_END();
109108
}
@@ -156,6 +155,20 @@ static bool php_dom_xpath_is_callback_name_valid_and_throw(const zend_string *na
156155
return true;
157156
}
158157

158+
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)
159+
{
160+
if (registry->namespaces) {
161+
zend_string *namespace;
162+
php_dom_xpath_callback_ns *ns;
163+
ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(registry->namespaces, namespace, ns) {
164+
zend_string *name;
165+
ZEND_HASH_MAP_FOREACH_STR_KEY(&ns->functions, name) {
166+
register_func(ctxt, namespace, name);
167+
} ZEND_HASH_FOREACH_END();
168+
} ZEND_HASH_FOREACH_END();
169+
}
170+
}
171+
159172
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)
160173
{
161174
zval *entry, registered_value;

ext/dom/xpath_callbacks.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ typedef enum {
3434
} php_dom_xpath_nodeset_evaluation_mode;
3535

3636
typedef void (*php_dom_xpath_callbacks_proxy_factory)(xmlNodePtr node, zval *proxy, dom_object *intern, xmlXPathParserContextPtr ctxt);
37-
typedef void (*php_dom_xpath_callbacks_register_func_ctx)(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name);
37+
typedef void (*php_dom_xpath_callbacks_register_func_ctx)(void *ctxt, const zend_string *ns, const zend_string *name);
3838

3939
typedef struct {
4040
HashTable functions;
@@ -58,6 +58,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callba
5858
PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args);
5959
PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer);
6060
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);
61+
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);
6162
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);
6263
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);
6364
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);

ext/xsl/php_xsl.stub.php

+2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ public function hasExsltSupport(): bool {}
114114
/** @tentative-return-type */
115115
public function registerPHPFunctions(array|string|null $functions = null): void {}
116116

117+
public function registerPHPFunctionsNS(string $namespace, array|string $functions): void {}
118+
117119
/** @return true */
118120
public function setProfiling(?string $filename) {} // TODO make return type void
119121

ext/xsl/php_xsl_arginfo.h

+8-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
--TEST--
2+
registerPHPFunctionsNS() function
3+
--EXTENSIONS--
4+
xsl
5+
--FILE--
6+
<?php
7+
8+
function createProcessor($inputs) {
9+
$xsl = new DomDocument();
10+
$xsl->loadXML('<?xml version="1.0" encoding="iso-8859-1" ?>
11+
<xsl:stylesheet version="1.0"
12+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
13+
xmlns:foo="urn:foo"
14+
xmlns:bar="urn:bar">
15+
<xsl:template match="//a">'
16+
. implode('', array_map(fn($input) => '<xsl:value-of select="' . $input . '" />', $inputs)) .
17+
'</xsl:template>
18+
</xsl:stylesheet>');
19+
20+
$proc = new XSLTProcessor();
21+
$proc->importStylesheet($xsl);
22+
return $proc;
23+
}
24+
25+
$inputdom = new DomDocument();
26+
$inputdom->loadXML('<?xml version="1.0" encoding="iso-8859-1"?><a href="https://php.net">hello</a>');
27+
28+
echo "--- Legit cases: none ---\n";
29+
30+
$proc = createProcessor(["foo:var_dump(string(@href))"]);
31+
try {
32+
$proc->transformToXml($inputdom);
33+
} catch (Error $e) {
34+
echo $e->getMessage(), "\n";
35+
}
36+
37+
echo "--- Legit cases: string callable ---\n";
38+
39+
$proc = createProcessor(["foo:var_dump(string(@href))"]);
40+
$proc->registerPHPFunctionsNS('urn:foo', 'var_dump');
41+
$proc->transformToXml($inputdom);
42+
43+
echo "--- Legit cases: string callable in array ---\n";
44+
45+
$proc = createProcessor(["foo:var_dump(string(@href))"]);
46+
$proc->registerPHPFunctionsNS('urn:foo', ['var_dump']);
47+
$proc->transformToXml($inputdom);
48+
49+
echo "--- Legit cases: callable in array ---\n";
50+
51+
$proc = createProcessor(["foo:test(string(@href))"]);
52+
$proc->registerPHPFunctionsNS('urn:foo', ['test' => var_dump(...)]);
53+
$proc->transformToXml($inputdom);
54+
55+
echo "--- Legit cases: multiple namespaces ---\n";
56+
57+
$proc = createProcessor(["foo:test(string(@href))", "bar:test(string(@href))"]);
58+
$proc->registerPHPFunctionsNS('urn:foo', ['test' => strrev(...)]);
59+
$proc->registerPHPFunctionsNS('urn:bar', ['test' => strtoupper(...)]);
60+
var_dump($proc->transformToXml($inputdom));
61+
62+
echo "--- Error cases ---\n";
63+
64+
try {
65+
createProcessor([])->registerPhpFunctionsNS('http://php.net/xsl', ['strtolower']);
66+
} catch (ValueError $e) {
67+
echo $e->getMessage(), "\n";
68+
}
69+
70+
try {
71+
createProcessor([])->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
72+
} catch (ValueError $e) {
73+
echo $e->getMessage(), "\n";
74+
}
75+
76+
try {
77+
createProcessor([])->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
78+
} catch (ValueError $e) {
79+
echo $e->getMessage(), "\n";
80+
}
81+
82+
try {
83+
createProcessor([])->registerPhpFunctionsNS("\0", ['strtolower']);
84+
} catch (ValueError $e) {
85+
echo $e->getMessage(), "\n";
86+
}
87+
88+
try {
89+
createProcessor([])->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
90+
} catch (Error $e) {
91+
echo $e->getMessage(), "\n";
92+
}
93+
94+
?>
95+
--EXPECTF--
96+
--- Legit cases: none ---
97+
98+
Warning: XSLTProcessor::transformToXml(): xmlXPathCompOpEval: function var_dump not found in %s on line %d
99+
100+
Warning: XSLTProcessor::transformToXml(): Unregistered function in %s on line %d
101+
102+
Warning: XSLTProcessor::transformToXml(): runtime error: file %s line 6 element value-of in %s on line %d
103+
104+
Warning: XSLTProcessor::transformToXml(): XPath evaluation returned no result. in %s on line %d
105+
--- Legit cases: string callable ---
106+
string(15) "https://php.net"
107+
--- Legit cases: string callable in array ---
108+
string(15) "https://php.net"
109+
--- Legit cases: callable in array ---
110+
string(15) "https://php.net"
111+
--- Legit cases: multiple namespaces ---
112+
string(53) "<?xml version="1.0"?>
113+
ten.php//:sptthHTTPS://PHP.NET
114+
"
115+
--- Error cases ---
116+
XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xsl" because it is reserved for PHP
117+
XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
118+
XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
119+
XSLTProcessor::registerPHPFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
120+
Object of class Closure could not be converted to string

ext/xsl/xsltprocessor.c

+59-10
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,34 @@ static void xsl_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern,
6565
php_dom_create_object(node, child, intern);
6666
}
6767

68-
static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
68+
static xsl_object *xsl_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
6969
{
70-
bool error = false;
71-
xsl_object *intern;
72-
73-
if (! zend_is_executing()) {
70+
if (!zend_is_executing()) {
7471
xsltGenericError(xsltGenericErrorContext,
7572
"xsltExtFunctionTest: Function called from outside of PHP\n");
76-
error = true;
7773
} else {
7874
xsltTransformContextPtr tctxt = xsltXPathGetTransformContext(ctxt);
7975
if (tctxt == NULL) {
8076
xsltGenericError(xsltGenericErrorContext,
8177
"xsltExtFunctionTest: failed to get the transformation context\n");
82-
error = true;
8378
} else {
84-
intern = (xsl_object*)tctxt->_private;
79+
xsl_object *intern = (xsl_object *) tctxt->_private;
8580
if (intern == NULL) {
8681
xsltGenericError(xsltGenericErrorContext,
8782
"xsltExtFunctionTest: failed to get the internal object\n");
88-
error = true;
83+
return NULL;
8984
}
85+
return intern;
9086
}
9187
}
9288

93-
if (error) {
89+
return NULL;
90+
}
91+
92+
static void xsl_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
93+
{
94+
xsl_object *intern = xsl_ext_fetch_intern(ctxt);
95+
if (!intern) {
9496
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
9597
} else {
9698
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) /* {{
110112
}
111113
/* }}} */
112114

115+
static void xsl_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
116+
{
117+
xsl_object *intern = xsl_ext_fetch_intern(ctxt);
118+
if (!intern) {
119+
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
120+
} else {
121+
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);
122+
}
123+
}
124+
113125
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#
114126
Since:
115127
*/
@@ -193,6 +205,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
193205
}
194206
/* }}} end XSLTProcessor::importStylesheet */
195207

208+
static void php_xsl_delayed_lib_registration(void *ctxt, const zend_string *ns, const zend_string *name)
209+
{
210+
xsltTransformContextPtr xsl = (xsltTransformContextPtr) ctxt;
211+
xsltRegisterExtFunction(xsl, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), xsl_ext_function_trampoline);
212+
}
213+
196214
static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStylesheetPtr style, zval *docp) /* {{{ */
197215
{
198216
xmlDocPtr newdocp = NULL;
@@ -299,6 +317,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
299317
}
300318
}
301319

320+
php_dom_xpath_callbacks_delayed_lib_registration(&intern->xpath_callbacks, ctxt, php_xsl_delayed_lib_registration);
321+
302322
if (secPrefsError == 1) {
303323
php_error_docref(NULL, E_WARNING, "Can't set libxslt security properties, not doing transformation for security reasons");
304324
} else {
@@ -592,6 +612,35 @@ PHP_METHOD(XSLTProcessor, registerPHPFunctions)
592612
}
593613
/* }}} end XSLTProcessor::registerPHPFunctions(); */
594614

615+
PHP_METHOD(XSLTProcessor, registerPHPFunctionsNS)
616+
{
617+
xsl_object *intern = Z_XSL_P(ZEND_THIS);
618+
619+
zend_string *namespace;
620+
zend_string *callable_name;
621+
HashTable *callable_ht;
622+
623+
ZEND_PARSE_PARAMETERS_START(2, 2)
624+
Z_PARAM_PATH_STR(namespace)
625+
Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
626+
ZEND_PARSE_PARAMETERS_END();
627+
628+
if (zend_string_equals_literal(namespace, "http://php.net/xsl")) {
629+
zend_argument_value_error(1, "must not be \"http://php.net/xsl\" because it is reserved for PHP");
630+
RETURN_THROWS();
631+
}
632+
633+
php_dom_xpath_callbacks_update_method_handler(
634+
&intern->xpath_callbacks,
635+
NULL,
636+
namespace,
637+
callable_name,
638+
callable_ht,
639+
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
640+
NULL
641+
);
642+
}
643+
595644
/* {{{ */
596645
PHP_METHOD(XSLTProcessor, setProfiling)
597646
{

0 commit comments

Comments
 (0)