Skip to content

Commit

Permalink
Implement namespaced functions for DOM
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsdos committed Nov 30, 2023
1 parent f4cd4d3 commit 542ae01
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 34 deletions.
2 changes: 2 additions & 0 deletions ext/dom/php_dom.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,8 @@ public function registerNamespace(string $prefix, string $namespace): bool {}

/** @tentative-return-type */
public function registerPhpFunctions(string|array|null $restrict = null): void {}

public function registerPhpFunctionsNS(string $namespace, string|array $restrict): void {}
}
#endif

Expand Down
13 changes: 12 additions & 1 deletion ext/dom/php_dom_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion ext/dom/tests/DOMXPath_callables.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ try {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctions(["\0" => var_dump(...)]);
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctions("");
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
--- Legit cases: none ---
Expand All @@ -131,4 +143,6 @@ DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be of type array|
Object of class Closure could not be converted to string
Object of class Closure could not be converted to string
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array with valid callbacks as values, function "nonexistent" not found or invalid function name
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) array key must not be empty
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be an array containing valid callback names
DOMXPath::registerPhpFunctions(): Argument #1 ($restrict) must be a valid callback name
91 changes: 91 additions & 0 deletions ext/dom/tests/registerPhpFunctionsNS.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
--TEST--
registerPhpFunctionsNS() function
--EXTENSIONS--
dom
--FILE--
<?php

$doc = new DOMDocument();
$doc->loadHTML('<a href="https://PHP.net">hello</a>');

$xpath = new DOMXPath($doc);

echo "--- Error cases ---\n";

try {
$xpath->registerPhpFunctionsNS('http://php.net/xpath', ['strtolower']);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctionsNS('urn:foo', ['x:a' => 'strtolower']);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctionsNS("urn:foo", ["\0" => 'strtolower']);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctionsNS("\0", ['strtolower']);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

try {
$xpath->registerPhpFunctionsNS("urn:foo", [var_dump(...)]);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

echo "--- Legit cases: string callable ---\n";

$xpath->registerNamespace('foo', 'urn:foo');
$xpath->registerPhpFunctionsNS('urn:foo', 'strtolower');
var_dump($xpath->query('//a[foo:strtolower(string(@href)) = "https://php.net"]'));

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

$xpath->registerPhpFunctionsNS('urn:foo', ['strtoupper']);
var_dump($xpath->query('//a[foo:strtoupper(string(@href)) = "https://php.net"]'));

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

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

echo "--- Legit cases: multiple namespaces ---\n";

$xpath->registerNamespace('bar', 'urn:bar');
$xpath->registerPhpFunctionsNS('urn:bar', ['test' => 'strtolower']);
var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]'));

?>
--EXPECT--
--- Error cases ---
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not be "http://php.net/xpath" because it is reserved for PHP
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must be an array containing valid callback names
DOMXPath::registerPhpFunctionsNS(): Argument #1 ($namespace) must not contain any null bytes
Object of class Closure could not be converted to string
--- Legit cases: string callable ---
object(DOMNodeList)#6 (1) {
["length"]=>
int(1)
}
--- Legit cases: string callable in array ---
object(DOMNodeList)#6 (1) {
["length"]=>
int(0)
}
--- Legit cases: callable in array ---
string(15) "https://PHP.net"
--- Legit cases: multiple namespaces ---
object(DOMNodeList)#4 (1) {
["length"]=>
int(1)
}
80 changes: 67 additions & 13 deletions ext/dom/xpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,30 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in
php_dom_create_object(node, child, intern);
}

static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
{
bool error = false;
dom_xpath_object *intern;

if (! zend_is_executing()) {
if (!zend_is_executing()) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: Function called from outside of PHP\n");
error = true;
} else {
intern = (dom_xpath_object *) ctxt->context->userData;
dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
if (intern == NULL) {
xmlGenericError(xmlGenericErrorContext,
"xmlExtFunctionTest: failed to get the internal object\n");
error = true;
return NULL;
}
return intern;
}
return NULL;
}

if (error) {
static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
{
dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
if (!intern) {
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
php_dom_xpath_callbacks_call(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
}
}
/* }}} */
Expand All @@ -99,6 +101,16 @@ static void dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt, int
}
/* }}} */

static void dom_xpath_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
{
dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
if (!intern) {
php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
} else {
php_dom_xpath_callbacks_call_custom_ns(&intern->xpath_callbacks, ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET, &intern->dom, dom_xpath_proxy_factory);
}
}

/* {{{ */
PHP_METHOD(DOMXPath, __construct)
{
Expand Down Expand Up @@ -378,18 +390,60 @@ PHP_METHOD(DOMXPath, registerPhpFunctions)
{
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);

zend_string *name = NULL;
zend_string *callable_name = NULL;
HashTable *callable_ht = NULL;

ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, name)
Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, callable_name)
ZEND_PARSE_PARAMETERS_END();

php_dom_xpath_callbacks_update_method_handler(&intern->xpath_callbacks, NULL, name, callable_ht);
php_dom_xpath_callbacks_update_method_handler(
&intern->xpath_callbacks,
intern->dom.ptr,
NULL,
callable_name,
callable_ht,
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
NULL
);
}
/* }}} end dom_xpath_register_php_functions */

static void dom_xpath_register_func_in_ctx(xmlXPathContextPtr ctxt, const zend_string *ns, const zend_string *name)
{
xmlXPathRegisterFuncNS(ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
}

PHP_METHOD(DOMXPath, registerPhpFunctionsNS)
{
dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);

zend_string *namespace;
zend_string *callable_name;
HashTable *callable_ht;

ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_PATH_STR(namespace)
Z_PARAM_ARRAY_HT_OR_STR(callable_ht, callable_name)
ZEND_PARSE_PARAMETERS_END();

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

php_dom_xpath_callbacks_update_method_handler(
&intern->xpath_callbacks,
intern->dom.ptr,
namespace,
callable_name,
callable_ht,
PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
dom_xpath_register_func_in_ctx
);
}

#endif /* LIBXML_XPATH_ENABLED */

#endif
Loading

0 comments on commit 542ae01

Please sign in to comment.