Skip to content

Commit 542ae01

Browse files
committed
Implement namespaced functions for DOM
1 parent f4cd4d3 commit 542ae01

9 files changed

+315
-34
lines changed

Diff for: ext/dom/php_dom.stub.php

+2
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,8 @@ public function registerNamespace(string $prefix, string $namespace): bool {}
932932

933933
/** @tentative-return-type */
934934
public function registerPhpFunctions(string|array|null $restrict = null): void {}
935+
936+
public function registerPhpFunctionsNS(string $namespace, string|array $restrict): void {}
935937
}
936938
#endif
937939

Diff for: ext/dom/php_dom_arginfo.h

+12-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ext/dom/tests/DOMXPath_callables.phpt

+15-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ try {
109109
echo $e->getMessage(), "\n";
110110
}
111111

112+
try {
113+
$xpath->registerPhpFunctions(["\0" => var_dump(...)]);
114+
} catch (Throwable $e) {
115+
echo $e->getMessage(), "\n";
116+
}
117+
118+
try {
119+
$xpath->registerPhpFunctions("");
120+
} catch (Throwable $e) {
121+
echo $e->getMessage(), "\n";
122+
}
123+
112124
?>
113125
--EXPECT--
114126
--- Legit cases: none ---
@@ -131,4 +143,6 @@ DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|
131143
Object of class Closure could not be converted to string
132144
Object of class Closure could not be converted to string
133145
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
134-
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) array key must not be empty
146+
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
147+
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
148+
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a valid callback name

Diff for: ext/dom/tests/registerPhpFunctionsNS.phpt

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
--TEST--
2+
registerPhpFunctionsNS() function
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$doc = new DOMDocument();
9+
$doc->loadHTML('<a href="https://PHP.net">hello</a>');
10+
11+
$xpath = new DOMXPath($doc);
12+
13+
echo "--- Error cases ---\n";
14+
15+
try {
16+
$xpath->registerPhpFunctionsNS('http://php.net/xpath', ['strtolower']);
17+
} catch (ValueError $e) {
18+
echo $e->getMessage(), "\n";
19+
}
20+
21+
try {
22+
$xpath->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
23+
} catch (ValueError $e) {
24+
echo $e->getMessage(), "\n";
25+
}
26+
27+
try {
28+
$xpath->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
29+
} catch (ValueError $e) {
30+
echo $e->getMessage(), "\n";
31+
}
32+
33+
try {
34+
$xpath->registerPhpFunctionsNS("\0", ['strtolower']);
35+
} catch (ValueError $e) {
36+
echo $e->getMessage(), "\n";
37+
}
38+
39+
try {
40+
$xpath->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
41+
} catch (Error $e) {
42+
echo $e->getMessage(), "\n";
43+
}
44+
45+
echo "--- Legit cases: string callable ---\n";
46+
47+
$xpath->registerNamespace('foo', 'urn:foo');
48+
$xpath->registerPhpFunctionsNS('urn:foo', 'strtolower');
49+
var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));
50+
51+
echo "--- Legit cases: string callable in array ---\n";
52+
53+
$xpath->registerPhpFunctionsNS('urn:foo', ['strtoupper']);
54+
var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]'));
55+
56+
echo "--- Legit cases: callable in array ---\n";
57+
58+
$xpath->registerPhpFunctionsNS('urn:foo', ['test' => 'var_dump']);
59+
$xpath->query('//a[foo:test(string(@href))]');
60+
61+
echo "--- Legit cases: multiple namespaces ---\n";
62+
63+
$xpath->registerNamespace('bar', 'urn:bar');
64+
$xpath->registerPhpFunctionsNS('urn:bar', ['test' => 'strtolower']);
65+
var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));
66+
67+
?>
68+
--EXPECT--
69+
--- Error cases ---
70+
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xpath" because it is reserved for PHP
71+
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
72+
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
73+
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
74+
Object of class Closure could not be converted to string
75+
--- Legit cases: string callable ---
76+
object(DOMNodeList)#6 (1) {
77+
["length"]=>
78+
int(1)
79+
}
80+
--- Legit cases: string callable in array ---
81+
object(DOMNodeList)#6 (1) {
82+
["length"]=>
83+
int(0)
84+
}
85+
--- Legit cases: callable in array ---
86+
string(15) "https://PHP.net"
87+
--- Legit cases: multiple namespaces ---
88+
object(DOMNodeList)#4 (1) {
89+
["length"]=>
90+
int(1)
91+
}

Diff for: ext/dom/xpath.c

+67-13
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,30 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in
6161
php_dom_create_object(node, child, intern);
6262
}
6363

64-
static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
64+
static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
6565
{
66-
bool error = false;
67-
dom_xpath_object *intern;
68-
69-
if (! zend_is_executing()) {
66+
if (!zend_is_executing()) {
7067
xmlGenericError(xmlGenericErrorContext,
7168
"xmlExtFunctionTest: Function called from outside of PHP\n");
72-
error = true;
7369
} else {
74-
intern = (dom_xpath_object *) ctxt->context->userData;
70+
dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
7571
if (intern == NULL) {
7672
xmlGenericError(xmlGenericErrorContext,
7773
"xmlExtFunctionTest: failed to get the internal object\n");
78-
error = true;
74+
return NULL;
7975
}
76+
return intern;
8077
}
78+
return NULL;
79+
}
8180

82-
if (error) {
81+
static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
82+
{
83+
dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
84+
if (!intern) {
8385
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
8486
} else {
85-
php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
87+
php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
8688
}
8789
}
8890
/* }}} */
@@ -99,6 +101,16 @@ static void dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt, int
99101
}
100102
/* }}} */
101103

104+
static void dom_xpath_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
105+
{
106+
dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
107+
if (!intern) {
108+
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
109+
} else {
110+
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);
111+
}
112+
}
113+
102114
/* {{{ */
103115
PHP_METHOD(DOMXPath, __construct)
104116
{
@@ -378,18 +390,60 @@ PHP_METHOD(DOMXPath, registerPhpFunctions)
378390
{
379391
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
380392

381-
zend_string *name = NULL;
393+
zend_string *callable_name = NULL;
382394
HashTable *callable_ht = NULL;
383395

384396
ZEND_PARSE_PARAMETERS_START(0, 1)
385397
Z_PARAM_OPTIONAL
386-
Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
398+
Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, callable_name)
387399
ZEND_PARSE_PARAMETERS_END();
388400

389-
php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
401+
php_dom_xpath_callbacks_update_method_handler(
402+
&intern->xpath_callbacks,
403+
intern->dom.ptr,
404+
NULL,
405+
callable_name,
406+
callable_ht,
407+
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
408+
NULL
409+
);
390410
}
391411
/* }}} end dom_xpath_register_php_functions */
392412

413+
static void dom_xpath_register_func_in_ctx(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name)
414+
{
415+
xmlXPathRegisterFuncNS(ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
416+
}
417+
418+
PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
419+
{
420+
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
421+
422+
zend_string *namespace;
423+
zend_string *callable_name;
424+
HashTable *callable_ht;
425+
426+
ZEND_PARSE_PARAMETERS_START(2, 2)
427+
Z_PARAM_PATH_STR(namespace)
428+
Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
429+
ZEND_PARSE_PARAMETERS_END();
430+
431+
if (zend_string_equals_literal(namespace, "http://php.net/xpath")) { // TODO: this is different for XSL!!!
432+
zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved for PHP");
433+
RETURN_THROWS();
434+
}
435+
436+
php_dom_xpath_callbacks_update_method_handler(
437+
&intern->xpath_callbacks,
438+
intern->dom.ptr,
439+
namespace,
440+
callable_name,
441+
callable_ht,
442+
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
443+
dom_xpath_register_func_in_ctx
444+
);
445+
}
446+
393447
#endif /* LIBXML_XPATH_ENABLED */
394448

395449
#endif

0 commit comments

Comments
 (0)