Permalink
Browse files

feature(web_services): allows API function to be given an associative…

… array

An additional flag allows the API function to receive a single array of
named parameters.

Any callable can be used as the API function.

Missing parameters with no default value now are passed as `null` to the
API function. No longer are the remaining arguments shifted left.

Fixes #9411
Conflicts:
	mod/web_services/tests/ElggCoreWebServicesApiTest.php
  • Loading branch information...
mrclay committed Feb 24, 2016
1 parent d41772c commit cd80863a5e91e4184080bdf451c26ee10058febd
@@ -137,6 +137,49 @@ You can use additional fields to describe your parameter, e.g. ``description``.
false
);
.. note::
If a missing parameter has no default value, the argument will be ``null``. Before 2.1, a bug caused later
arguments to be shifted left in this case.
Receive parameters as associative array
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you have a large number of method parameters, you can force the execution script
to invoke the callback function with a single argument that contains an associative
array of parameter => input pairs (instead of each parameter being a separate argument).
To do that, set ``$assoc`` to ``true`` in ``elgg_ws_expose_function()``.
.. code:: php
function greet_me($values) {
$name = elgg_extract('name', $values);
$greeting = elgg_extract('greeting', $values, 'Hello');
return "$greeting, $name";
}
elgg_ws_expose_function(
"test.greet",
"greet_me",
[
"name" => [
'type' => 'string',
],
"greeting" => [
'type' => 'string',
'default' => 'Hello',
'required' => false,
],
],
'A testing method which echos a greeting',
'GET',
false,
false,
true // $assoc makes the callback receive an associative array
);
.. note:: If a missing parameter has no default value, ``null`` will be used.
API authentication
------------------
@@ -64,18 +64,8 @@ function execute_method($method) {
}
// function must be callable
$function = null;
if (isset($API_METHODS[$method]["function"])) {
$function = $API_METHODS[$method]["function"];
// allow array version of static callback
if (is_array($function)
&& isset($function[0], $function[1])
&& is_string($function[0])
&& is_string($function[1])) {
$function = "{$function[0]}::{$function[1]}";
}
}
if (!is_string($function) || !is_callable($function)) {
$function = elgg_extract('function', $API_METHODS[$method]);
if (!$function || !is_callable($function)) {
$msg = elgg_echo('APIException:FunctionDoesNotExist', array($method));
throw new APIException($msg);
}
@@ -97,8 +87,14 @@ function execute_method($method) {
// Execute function: Construct function and calling parameters
$serialised_parameters = trim($serialised_parameters, ", ");
// @todo remove the need for eval()
$result = eval("return $function($serialised_parameters);");
$arguments = eval("return [$serialised_parameters];");
if ($API_METHODS[$method]['assoc']) {
$argument = array_combine(_elgg_ws_get_parameter_names($method), $arguments);
$result = call_user_func($function, $argument);
} else {
$result = call_user_func_array($function, $arguments);
}
$result = elgg_trigger_plugin_hook('rest:output', $method, $parameters, $result);
@@ -219,6 +215,23 @@ function verify_parameters($method, $parameters) {
return true;
}
/**
* Get the names of a method's parameters
*
* @param string $method
* @return string[]
* @access private
*/
function _elgg_ws_get_parameter_names($method) {
global $API_METHODS;
if (!isset($API_METHODS[$method]["parameters"])) {
return [];
}
return array_keys($API_METHODS[$method]["parameters"]);
}
/**
* Serialize an array of parameters for an API method call
*
@@ -243,6 +256,7 @@ function serialise_parameters($method, $parameters) {
// avoid warning on parameters that are not required and not present
if (!isset($parameters[$key])) {
$serialised_parameters .= ',null';
continue;
}
View
@@ -111,37 +111,48 @@ function ws_page_handler($segments) {
* It also cannot handle arrays of bools or arrays of arrays.
* Also, input will be filtered to protect against XSS attacks through the web services.
*
* @param string $method The api name to expose - for example "myapi.dosomething"
* @param string $function Your function callback.
* @param array $parameters (optional) List of parameters in the same order as in
* your function. Default values may be set for parameters which
* allow REST api users flexibility in what parameters are passed.
* Generally, optional parameters should be after required
* parameters.
* @param string $method The api name to expose - for example "myapi.dosomething"
* @param callable $function Callable to handle API call
* @param array $parameters (optional) List of parameters in the same order as in
* your function. Default values may be set for parameters which
* allow REST api users flexibility in what parameters are passed.
* Generally, optional parameters should be after required
* parameters. If an optional parameter is not set and has no default,
* the API callable will receive null.
*
* This array should be in the format
* "variable" = array (
* type => 'int' | 'bool' | 'float' | 'string' | 'array'
* required => true (default) | false
* default => value (optional)
* This array should be in the format
* "variable" = array (
* type => 'int' | 'bool' | 'float' | 'string' | 'array'
* required => true (default) | false
* default => value (optional)
* )
* @param string $description (optional) human readable description of the function.
* @param string $call_method (optional) Define what http method must be used for
* this function. Default: GET
* @param bool $require_api_auth (optional) (default is false) Does this method
* require API authorization? (example: API key)
* @param bool $require_user_auth (optional) (default is false) Does this method
* require user authorization?
* @param string $description (optional) human readable description of the function.
* @param string $call_method (optional) Define what http method must be used for
* this function. Default: GET
* @param bool $require_api_auth (optional) (default is false) Does this method
* require API authorization? (example: API key)
* @param bool $require_user_auth (optional) (default is false) Does this method
* require user authorization?
* @param bool $assoc (optional) If set to true, the callback function will receive a single argument
* that contains an associative array of parameter => input pairs for the method.
*
* @return bool
* @throws InvalidParameterException
*/
function elgg_ws_expose_function($method, $function, array $parameters = NULL, $description = "",
$call_method = "GET", $require_api_auth = false, $require_user_auth = false) {
function elgg_ws_expose_function(
$method,
$function,
array $parameters = null,
$description = "",
$call_method = "GET",
$require_api_auth = false,
$require_user_auth = false,
$assoc = false
) {
global $API_METHODS;
if (($method == "") || ($function == "")) {
if (($method == "") || !$function) {
$msg = elgg_echo('InvalidParameterException:APIMethodOrFunctionNotSet');
throw new InvalidParameterException($msg);
}
@@ -199,6 +210,8 @@ function elgg_ws_expose_function($method, $function, array $parameters = NULL, $
$API_METHODS[$method]["require_user_auth"] = $require_user_auth;
$API_METHODS[$method]["assoc"] = (bool)$assoc;
return true;
}
@@ -85,6 +85,7 @@ public function testExposeFunctionSuccess() {
$method['call_method'] = 'GET';
$method['require_api_auth'] = false;
$method['require_user_auth'] = false;
$method['assoc'] = false;
$this->assertIdentical($method, $API_METHODS['test']);
}
@@ -274,6 +275,15 @@ public function testSerialiseParameters() {
$s = serialise_parameters('test', $parameters);
$this->assertIdentical($s, ",array('0'=>'1','1'=>'2')");
// test missing optional param
$this->registerFunction(false, false, [
'param1' => ['type' => 'int', 'required' => false],
'param2' => ['type' => 'int'],
]);
$parameters = ['param2' => '2'];
$s = serialise_parameters('test', $parameters);
$this->assertIdentical($s, ",null,2");
// test unknown type
$this->registerFunction(false, false, array('param1' => array('type' => 'bad')));
$parameters = array('param1' => 'test');
@@ -297,8 +307,6 @@ public function testApiAuthKeyNoKey() {
}
public function testApiAuthKeyBadKey() {
global $CONFIG;
set_input('api_key', 'BAD');
try {
api_auth_key();
@@ -308,16 +316,117 @@ public function testApiAuthKeyBadKey() {
$this->assertIdentical($e->getMessage(), elgg_echo('APIException:BadAPIKey'));
}
}
public function testSerialiseParametersCasting() {
$types = [
'int' => [
["0", 0],
["1", 1],
[" 1", 1],
],
'bool' => [
["0", false],
[" 1", true],
// BC with 2.0
[" false", false],
["true", false],
],
'float' => [
["1.65", 1.65],
[" 1.65 ", 1.65],
],
'array' => [
[["2 ", " bar"], [2, "bar"]],
[["' \""], ["' \\\""]],
],
'string' => [
[" foo ", "foo"],
],
];
foreach ($types as $type => $tests) {
foreach ($tests as $test) {
set_input('param', $test[0]);
$this->registerFunction(false, false, [
'param' => ['type' => $type],
]);
$serialized = serialise_parameters('test', [
// get_input() necessary because it does recursive trimming
'param' => get_input('param'),
]);
$serialized = trim($serialized, ", ");
// evaled
$value = eval("return $serialized;");
$this->assertEqual($value, $test[1]);
}
}
}
public function testExecuteMethod() {
$params = array(
'param1' => array('type' => 'int', 'required' => false),
'param2' => array('type' => 'bool', 'required' => true),
);
elgg_ws_expose_function('test', array($this, 'methodCallback'), $params);
set_input('param1', "2");
set_input('param2', "1");
$result = execute_method('test');
$this->assertIsA($result, 'SuccessResult');
$this->assertIdentical($result->export()->result, array(2, true));
set_input('param1', null);
set_input('param2', "1");
$result = execute_method('test');
$this->assertIsA($result, 'SuccessResult');
$this->assertIdentical($result->export()->result, array(null, true));
}
public function testExecuteMethodAssoc() {
$params = array(
'param1' => array('type' => 'int', 'required' => false),
'param2' => array('type' => 'bool', 'required' => true),
);
elgg_ws_expose_function('test', array($this, 'methodCallbackAssoc'), $params, '', 'GET', false, false, true);
set_input('param1', "2");
set_input('param2', "1");
$result = execute_method('test');
$this->assertIsA($result, 'SuccessResult');
$this->assertIdentical($result->export()->result, array('param1' => 2, 'param2' => true));
set_input('param1', null);
set_input('param2', "1");
$result = execute_method('test');
$this->assertIsA($result, 'SuccessResult');
$this->assertIdentical($result->export()->result, array('param1' => null, 'param2' => true));
}
public function methodCallback() {
return func_get_args();
}
public function methodCallbackAssoc($values) {
return $values;
}
protected function registerFunction($api_auth = false, $user_auth = false, $params = null) {
protected function registerFunction($api_auth = false, $user_auth = false, $params = null, $assoc = false) {
$parameters = array('param1' => array('type' => 'int', 'required' => true),
'param2' => array('type' => 'bool', 'required' => false), );
if ($params == null) {
$params = $parameters;
}
elgg_ws_expose_function('test', 'elgg_echo', $params, '', 'POST', $api_auth, $user_auth);
$callback = ($assoc) ? [$this, 'methodCallbackAssoc'] : [$this, 'methodCallback'];
elgg_ws_expose_function('test', $callback, $params, '', 'POST', $api_auth, $user_auth);
}
}

0 comments on commit cd80863

Please sign in to comment.