aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorAndreas Gohr <andi@splitbrain.org>2023-11-30 19:57:15 +0100
committerAndreas Gohr <andi@splitbrain.org>2023-11-30 19:57:15 +0100
commitf8f551358486c13aff95feb66a23a63c91deae2a (patch)
treed606f40d58127928489d4b4721694a31c5a829d2
parent1d014596e2a2add42cbb5478ab42032bc5e03598 (diff)
downloaddokuwiki-f8f551358486c13aff95feb66a23a63c91deae2a.tar.gz
dokuwiki-f8f551358486c13aff95feb66a23a63c91deae2a.zip
First go at refactoring the API mechanisms
This introduces an ApiCall class that wraps around the actual method that produces the result. This replaces various loose array structures that provided the meta information before. The ApiCall streamlines the aggregation of meta information between core and plugin methods. Now all data is produced by Reflection based introspection. Certain aspects can be overridden if needed. See ApiCore::getRemoteInfo() for examples This change removes the _getMethods() method from remote plugins and introduces a getMethods() method. The two are NOT compatible as the latter now returns a list of ApiCalls. However when looking at the existing plugins, it seems that _getMethods() was nearly 100% obsolete with the Reflection based default implementation. So most plugins will not be affected at all. Some might now export one or two more methods than before because of poor visibility settings (eg. not declaring private/protected methods as such). This change removes the RPC_CALL_ADD hook. Only a single plugin ever implemented it. I'm not sure what this hook was supposed to do anyway. Being able to declare arbitrarily named API endpoints seems wrong to me anyway. The new ApiCall now also supports passing named instead of positional parameters. This will open up a new opportunity to get a proper openapi spec running. Next step is fixing the tests.
-rw-r--r--_test/tests/Remote/ApiCallTest.php98
-rw-r--r--inc/Extension/RemotePlugin.php65
-rw-r--r--inc/Remote/Api.php333
-rw-r--r--inc/Remote/ApiCall.php356
-rw-r--r--inc/Remote/ApiCore.php242
-rw-r--r--inc/Remote/JsonRpcServer.php10
6 files changed, 598 insertions, 506 deletions
diff --git a/_test/tests/Remote/ApiCallTest.php b/_test/tests/Remote/ApiCallTest.php
new file mode 100644
index 000000000..8f7404c2d
--- /dev/null
+++ b/_test/tests/Remote/ApiCallTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace dokuwiki\test\Remote;
+
+use dokuwiki\Remote\ApiCall;
+
+class ApiCallTest extends \DokuWikiTest
+{
+ /**
+ * This is a test
+ *
+ * With more information
+ * in several lines
+ * @param string $foo First variable
+ * @param int $bar
+ * @something else
+ * @something other
+ * @another tag
+ * @return string The return
+ */
+ public function dummyMethod1($foo, $bar)
+ {
+ return 'dummy';
+ }
+
+ public function testMethodDocBlock()
+ {
+ $call = new ApiCall([$this, 'dummyMethod1']);
+
+ $this->assertEquals('This is a test', $call->getSummary());
+ $this->assertEquals("With more information\nin several lines", $call->getDescription());
+
+ $this->assertEquals(
+ [
+ 'foo' => [
+ 'type' => 'string',
+ 'description' => 'First variable',
+ ],
+ 'bar' => [
+ 'type' => 'int',
+ 'description' => '',
+ ]
+ ],
+ $call->getArgs()
+ );
+
+ $this->assertEquals(
+ [
+ 'type' => 'string',
+ 'description' => 'The return'
+ ],
+ $call->getReturn()
+ );
+
+ // remove one parameter
+ $call->limitArgs(['foo']);
+ $this->assertEquals(
+ [
+ 'foo' => [
+ 'type' => 'string',
+ 'description' => 'First variable',
+ ],
+ ],
+ $call->getArgs()
+ );
+ }
+
+ public function testFunctionDocBlock()
+ {
+ $call = new ApiCall('date');
+ $call->setArgDescription('format', 'The format');
+
+ $this->assertEquals(
+ [
+ 'format' => [
+ 'type' => 'string',
+ 'description' => 'The format',
+ ],
+ 'timestamp' => [
+ 'type' => 'int',
+ 'description' => '',
+ ]
+ ],
+ $call->getArgs()
+ );
+ }
+
+ public function testExecution()
+ {
+ $call = new ApiCall([$this, 'dummyMethod1']);
+ $this->assertEquals('dummy', $call(['bar', 1]), 'positional parameters');
+ $this->assertEquals('dummy', $call(['foo' => 'bar', 'bar' => 1]), 'named parameters');
+
+ $call = new ApiCall('date');
+ $this->assertEquals('2023-11-30', $call(['Y-m-d', 1701356591]), 'positional parameters');
+ $this->assertEquals('2023-11-30', $call(['format' => 'Y-m-d', 'timestamp' => 1701356591]), 'named parameters');
+ }
+}
diff --git a/inc/Extension/RemotePlugin.php b/inc/Extension/RemotePlugin.php
index 52dccbc7d..f09faec6d 100644
--- a/inc/Extension/RemotePlugin.php
+++ b/inc/Extension/RemotePlugin.php
@@ -3,6 +3,7 @@
namespace dokuwiki\Extension;
use dokuwiki\Remote\Api;
+use dokuwiki\Remote\ApiCall;
use ReflectionException;
use ReflectionMethod;
@@ -29,10 +30,10 @@ abstract class RemotePlugin extends Plugin
* By default it exports all public methods of a remote plugin. Methods beginning
* with an underscore are skipped.
*
- * @return array Information about all provided methods. {@see dokuwiki\Remote\RemoteAPI}.
+ * @return ApiCall[] Information about all provided methods.
* @throws ReflectionException
*/
- public function _getMethods()
+ public function getMethods()
{
$result = [];
@@ -44,72 +45,30 @@ abstract class RemotePlugin extends Plugin
continue;
}
$method_name = $method->name;
- if (strpos($method_name, '_') === 0) {
+ if ($method_name[0] === '_') {
continue;
}
-
- // strip asterisks
- $doc = $method->getDocComment();
- $doc = preg_replace(
- ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'],
- ['', '', '', ''],
- $doc
- );
-
- // prepare data
- $data = [];
- $data['name'] = $method_name;
- $data['public'] = 0;
- $data['doc'] = $doc;
- $data['args'] = [];
-
- // get parameter type from doc block type hint
- foreach ($method->getParameters() as $parameter) {
- $name = $parameter->name;
- $type = 'string'; // we default to string
- if (preg_match('/^@param[ \t]+([\w|\[\]]+)[ \t]\$' . $name . '/m', $doc, $m)) {
- $type = $this->cleanTypeHint($m[1]);
- }
- $data['args'][] = $type;
- }
-
- // get return type from doc block type hint
- if (preg_match('/^@return[ \t]+([\w|\[\]]+)/m', $doc, $m)) {
- $data['return'] = $this->cleanTypeHint($m[1]);
- } else {
- $data['return'] = 'string';
+ if($method_name === 'getMethods') {
+ continue; // skip self, if overridden
}
// add to result
- $result[$method_name] = $data;
+ $result[$method_name] = new ApiCall([$this, $method_name]);
}
return $result;
}
/**
- * Matches the given type hint against the valid options for the remote API
- *
- * @param string $hint
- * @return string
+ * @deprecated 2023-11-30
*/
- protected function cleanTypeHint($hint)
+ public function _getMethods()
{
- $types = explode('|', $hint);
- foreach ($types as $t) {
- if (str_ends_with($t, '[]')) {
- return 'array';
- }
- if ($t === 'boolean') {
- return 'bool';
- }
- if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) {
- return $t;
- }
- }
- return 'string';
+ dbg_deprecated('getMethods()');
}
+
+
/**
* @return Api
*/
diff --git a/inc/Remote/Api.php b/inc/Remote/Api.php
index 2555157af..e3deab4cb 100644
--- a/inc/Remote/Api.php
+++ b/inc/Remote/Api.php
@@ -2,10 +2,8 @@
namespace dokuwiki\Remote;
-use dokuwiki\Extension\PluginInterface;
-use dokuwiki\Input\Input;
-use dokuwiki\Extension\Event;
use dokuwiki\Extension\RemotePlugin;
+use dokuwiki\Logger;
/**
* This class provides information about remote access to the wiki.
@@ -38,24 +36,11 @@ use dokuwiki\Extension\RemotePlugin;
*/
class Api
{
- /**
- * @var ApiCore|\RemoteAPICoreTest
- */
- private $coreMethods;
+ /** @var ApiCall[] core methods provided by dokuwiki */
+ protected $coreMethods;
- /**
- * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
- * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
- */
- private $pluginMethods;
-
- /**
- * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
- * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
- *
- * The remote method name is the same as in the remote name returned by _getMethods().
- */
- private $pluginCustomCalls;
+ /** @var ApiCall[] remote methods provided by dokuwiki plugins */
+ protected $pluginMethods;
private $dateTransformation;
private $fileTransformation;
@@ -72,7 +57,7 @@ class Api
/**
* Get all available methods with remote access.
*
- * @return array with information to all available methods
+ * @return ApiCall[] with information to all available methods
* @throws RemoteException
*/
public function getMethods()
@@ -81,273 +66,128 @@ class Api
}
/**
- * Call a method via remote api.
+ * Collects all the core methods
*
- * @param string $method name of the method to call.
- * @param array $args arguments to pass to the given method
- * @return mixed result of method call, must be a primitive type.
- * @throws RemoteException
+ * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing.
+ * Here you can pass a non-default RemoteAPICore instance. (for mocking)
+ * @return ApiCall[] all core methods.
*/
- public function call($method, $args = [])
+ public function getCoreMethods($apiCore = null)
{
- if ($args === null) {
- $args = [];
- }
- // Ensure we have at least one '.' in $method
- [$type, $pluginName, /* call */] = sexplode('.', $method . '.', 3, '');
- if ($type === 'plugin') {
- return $this->callPlugin($pluginName, $method, $args);
- }
- if ($this->coreMethodExist($method)) {
- return $this->callCoreMethod($method, $args);
+ if (!$this->coreMethods) {
+ if ($apiCore === null) {
+ $this->coreMethods = (new ApiCore($this))->getRemoteInfo();
+ } else {
+ $this->coreMethods = $apiCore->getRemoteInfo();
+ }
}
- return $this->callCustomCallPlugin($method, $args);
+ return $this->coreMethods;
}
/**
- * Check existance of core methods
+ * Collects all the methods of the enabled Remote Plugins
*
- * @param string $name name of the method
- * @return bool if method exists
+ * @return ApiCall[] all plugin methods.
*/
- private function coreMethodExist($name)
+ public function getPluginMethods()
{
- $coreMethods = $this->getCoreMethods();
- return array_key_exists($name, $coreMethods);
- }
+ if ($this->pluginMethods) return $this->pluginMethods;
- /**
- * Try to call custom methods provided by plugins
- *
- * @param string $method name of method
- * @param array $args
- * @return mixed
- * @throws RemoteException if method not exists
- */
- private function callCustomCallPlugin($method, $args)
- {
- $customCalls = $this->getCustomCallPlugins();
- if (!array_key_exists($method, $customCalls)) {
- throw new RemoteException('Method does not exist', -32603);
- }
- [$plugin, $method] = $customCalls[$method];
- $fullMethod = "plugin.$plugin.$method";
- return $this->callPlugin($plugin, $fullMethod, $args);
- }
+ $plugins = plugin_list('remote');
+ foreach ($plugins as $pluginName) {
+ /** @var RemotePlugin $plugin */
+ $plugin = plugin_load('remote', $pluginName);
+ if (!is_subclass_of($plugin, RemotePlugin::class)) {
+ Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin");
+ continue;
+ }
- /**
- * Returns plugin calls that are registered via RPC_CALL_ADD action
- *
- * @return array with pairs of custom plugin calls
- * @triggers RPC_CALL_ADD
- */
- private function getCustomCallPlugins()
- {
- if ($this->pluginCustomCalls === null) {
- $data = [];
- Event::createAndTrigger('RPC_CALL_ADD', $data);
- $this->pluginCustomCalls = $data;
+ try {
+ $methods = $plugin->getMethods();
+ } catch (\ReflectionException $e) {
+ Logger::error(
+ "Remote Plugin $pluginName failed to return methods",
+ $e->getMessage(),
+ $e->getFile(),
+ $e->getLine()
+ );
+ continue;
+ }
+
+ foreach ($methods as $method => $call) {
+ $this->pluginMethods["plugin.$pluginName.$method"] = $call;
+ }
}
- return $this->pluginCustomCalls;
+
+ return $this->pluginMethods;
}
/**
- * Call a plugin method
+ * Call a method via remote api.
*
- * @param string $pluginName
- * @param string $method method name
- * @param array $args
- * @return mixed return of custom method
+ * @param string $method name of the method to call.
+ * @param array $args arguments to pass to the given method
+ * @return mixed result of method call, must be a primitive type.
* @throws RemoteException
*/
- private function callPlugin($pluginName, $method, $args)
+ public function call($method, $args = [])
{
- $plugin = plugin_load('remote', $pluginName);
- $methods = $this->getPluginMethods();
- if (!$plugin instanceof PluginInterface) {
- throw new RemoteException('Method does not exist', -32603);
- }
- $this->checkAccess($methods[$method]);
- $name = $this->getMethodName($methods, $method);
- try {
- set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
- return call_user_func_array([$plugin, $name], $args);
- } catch (\ArgumentCountError $th) {
- throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
- } finally {
- restore_error_handler();
+ if ($args === null) {
+ $args = [];
}
- }
- /**
- * Call a core method
- *
- * @param string $method name of method
- * @param array $args
- * @return mixed
- * @throws RemoteException if method not exist
- */
- private function callCoreMethod($method, $args)
- {
- $coreMethods = $this->getCoreMethods();
- $this->checkAccess($coreMethods[$method]);
- if (!isset($coreMethods[$method])) {
+ // pre-flight checks
+ $this->ensureApiIsEnabled();
+ $methods = $this->getMethods();
+ if (!isset($methods[$method])) {
throw new RemoteException('Method does not exist', -32603);
}
- $this->checkArgumentLength($coreMethods[$method], $args);
+ $this->ensureAccessIsAllowed($methods[$method]);
+
+ // invoke the ApiCall
try {
- set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
- return call_user_func_array([$this->coreMethods, $this->getMethodName($coreMethods, $method)], $args);
+ return $methods[$method]($args);
} catch (\ArgumentCountError $th) {
throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
- } finally {
- restore_error_handler();
}
}
/**
- * Check if access should be checked
+ * Check that the API is generally enabled
*
- * @param array $methodMeta data about the method
- * @throws AccessDeniedException
- */
- private function checkAccess($methodMeta)
- {
- if (!isset($methodMeta['public'])) {
- $this->forceAccess();
- } elseif ($methodMeta['public'] == '0') {
- $this->forceAccess();
- }
- }
-
- /**
- * Check the number of parameters
- *
- * @param array $methodMeta data about the method
- * @param array $args
- * @throws RemoteException if wrong parameter count
- */
- private function checkArgumentLength($methodMeta, $args)
- {
- if (count($methodMeta['args']) < count($args)) {
- throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
- }
- }
-
- /**
- * Determine the name of the real method
- *
- * @param array $methodMeta list of data of the methods
- * @param string $method name of method
- * @return string
- */
- private function getMethodName($methodMeta, $method)
- {
- if (isset($methodMeta[$method]['name'])) {
- return $methodMeta[$method]['name'];
- }
- $method = explode('.', $method);
- return $method[count($method) - 1];
- }
-
- /**
- * Perform access check for current user
- *
- * @return bool true if the current user has access to remote api.
- * @throws AccessDeniedException If remote access disabled
+ * @return void
+ * @throws RemoteException thrown when the API is disabled
*/
- public function hasAccess()
+ protected function ensureApiIsEnabled()
{
global $conf;
- global $USERINFO;
- /** @var Input $INPUT */
- global $INPUT;
-
- if (!$conf['remote']) {
- throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
- }
- if (trim($conf['remoteuser']) == '!!not set!!') {
- return false;
- }
- if (!$conf['useacl']) {
- return true;
- }
- if (trim($conf['remoteuser']) == '') {
- return true;
+ if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') {
+ throw new RemoteException('Server Error. API is not enabled in config.', -32604);
}
-
- return auth_isMember(
- $conf['remoteuser'],
- $INPUT->server->str('REMOTE_USER'),
- (array)($USERINFO['grps'] ?? [])
- );
}
/**
- * Requests access
+ * Check if the current user is allowed to call the given method
*
+ * @param ApiCall $method
* @return void
- * @throws AccessDeniedException On denied access.
+ * @throws AccessDeniedException Thrown when the user is not allowed to call the method
*/
- public function forceAccess()
+ protected function ensureAccessIsAllowed(ApiCall $method)
{
- if (!$this->hasAccess()) {
- throw new AccessDeniedException('server error. not authorized to call method', -32604);
- }
- }
-
- /**
- * Collects all the methods of the enabled Remote Plugins
- *
- * @return array all plugin methods.
- * @throws RemoteException if not implemented
- */
- public function getPluginMethods()
- {
- if ($this->pluginMethods === null) {
- $this->pluginMethods = [];
- $plugins = plugin_list('remote');
-
- foreach ($plugins as $pluginName) {
- /** @var RemotePlugin $plugin */
- $plugin = plugin_load('remote', $pluginName);
- if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
- throw new RemoteException(
- "Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"
- );
- }
-
- try {
- $methods = $plugin->_getMethods();
- } catch (\ReflectionException $e) {
- throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
- }
+ global $conf;
+ global $INPUT;
+ global $USERINFO;
- foreach ($methods as $method => $meta) {
- $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
- }
- }
+ if ($method->isPublic()) return; // public methods are always allowed
+ if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users
+ if (trim($conf['remoteuser']) === '') return; // all users are allowed
+ if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) {
+ return; // user is allowed
}
- return $this->pluginMethods;
- }
- /**
- * Collects all the core methods
- *
- * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing.
- * Here you can pass a non-default RemoteAPICore instance. (for mocking)
- * @return array all core methods.
- */
- public function getCoreMethods($apiCore = null)
- {
- if ($this->coreMethods === null) {
- if ($apiCore === null) {
- $this->coreMethods = new ApiCore($this);
- } else {
- $this->coreMethods = $apiCore;
- }
- }
- return $this->coreMethods->getRemoteInfo();
+ // still here? no can do
+ throw new AccessDeniedException('server error. not authorized to call method', -32604);
}
/**
@@ -403,13 +243,4 @@ class Api
$this->fileTransformation = $fileTransformation;
}
- /**
- * The error handler that catches argument-related warnings
- */
- public function argumentWarningHandler($errno, $errstr)
- {
- if (str_starts_with($errstr, 'Missing argument ')) {
- throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
- }
- }
}
diff --git a/inc/Remote/ApiCall.php b/inc/Remote/ApiCall.php
new file mode 100644
index 000000000..144b6de2f
--- /dev/null
+++ b/inc/Remote/ApiCall.php
@@ -0,0 +1,356 @@
+<?php
+
+namespace dokuwiki\Remote;
+
+
+class ApiCall
+{
+ /** @var callable The method to be called for this endpoint */
+ protected $method;
+
+ /** @var bool Whether this call can be called without authentication */
+ protected bool $isPublic = false;
+
+ /** @var array Metadata on the accepted parameters */
+ protected array $args = [];
+
+ /** @var array Metadata on the return value */
+ protected array $return = [
+ 'type' => 'string',
+ 'description' => '',
+ ];
+
+ /** @var string The summary of the method */
+ protected string $summary = '';
+
+ /** @var string The description of the method */
+ protected string $description = '';
+
+ /**
+ * Make the given method available as an API call
+ *
+ * @param string|array $method Either [object,'method'] or 'function'
+ * @throws \ReflectionException
+ */
+ public function __construct($method)
+ {
+ if (!is_callable($method)) {
+ throw new \InvalidArgumentException('Method is not callable');
+ }
+
+ $this->method = $method;
+ $this->parseData();
+ }
+
+ /**
+ * Call the method
+ *
+ * Important: access/authentication checks need to be done before calling this!
+ *
+ * @param array $args
+ * @return mixed
+ */
+ public function __invoke($args)
+ {
+ if (!array_is_list($args)) {
+ $args = $this->namedArgsToPositional($args);
+ }
+ return call_user_func_array($this->method, $args);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isPublic(): bool
+ {
+ return $this->isPublic;
+ }
+
+ /**
+ * @param bool $isPublic
+ * @return $this
+ */
+ public function setPublic(bool $isPublic = true): self
+ {
+ $this->isPublic = $isPublic;
+ return $this;
+ }
+
+
+ /**
+ * @return array
+ */
+ public function getArgs(): array
+ {
+ return $this->args;
+ }
+
+ /**
+ * Limit the arguments to the given ones
+ *
+ * @param string[] $args
+ * @return $this
+ */
+ public function limitArgs($args): self
+ {
+ foreach ($args as $arg) {
+ if (!isset($this->args[$arg])) {
+ throw new \InvalidArgumentException("Unknown argument $arg");
+ }
+ }
+ $this->args = array_intersect_key($this->args, array_flip($args));
+
+ return $this;
+ }
+
+ /**
+ * Set the description for an argument
+ *
+ * @param string $arg
+ * @param string $description
+ * @return $this
+ */
+ public function setArgDescription(string $arg, string $description): self
+ {
+ if (!isset($this->args[$arg])) {
+ throw new \InvalidArgumentException('Unknown argument');
+ }
+ $this->args[$arg]['description'] = $description;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getReturn(): array
+ {
+ return $this->return;
+ }
+
+ /**
+ * Set the description for the return value
+ *
+ * @param string $description
+ * @return $this
+ */
+ public function setReturnDescription(string $description): self
+ {
+ $this->return['description'] = $description;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSummary(): string
+ {
+ return $this->summary;
+ }
+
+ /**
+ * @param string $summary
+ * @return $this
+ */
+ public function setSummary(string $summary): self
+ {
+ $this->summary = $summary;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return $this
+ */
+ public function setDescription(string $description): self
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ /**
+ * Fill in the metadata
+ *
+ * This uses Reflection to inspect the method signature and doc block
+ *
+ * @throws \ReflectionException
+ */
+ protected function parseData()
+ {
+ if (is_array($this->method)) {
+ $reflect = new \ReflectionMethod($this->method[0], $this->method[1]);
+ } else {
+ $reflect = new \ReflectionFunction($this->method);
+ }
+
+ $docInfo = $this->parseDocBlock($reflect->getDocComment());
+ $this->summary = $docInfo['summary'];
+ $this->description = $docInfo['description'];
+
+ foreach ($reflect->getParameters() as $parameter) {
+ $name = $parameter->name;
+ $realType = $parameter->getType();
+ if ($realType) {
+ $type = $realType->getName();
+ } elseif (isset($docInfo['args'][$name]['type'])) {
+ $type = $docInfo['args'][$name]['type'];
+ } else {
+ $type = 'string';
+ }
+
+ if (isset($docInfo['args'][$name]['description'])) {
+ $description = $docInfo['args'][$name]['description'];
+ } else {
+ $description = '';
+ }
+
+ $this->args[$name] = [
+ 'type' => $type,
+ 'description' => trim($description),
+ ];
+ }
+
+ $returnType = $reflect->getReturnType();
+ if ($returnType) {
+ $this->return['type'] = $returnType->getName();
+ } elseif (isset($docInfo['return']['type'])) {
+ $this->return['type'] = $docInfo['return']['type'];
+ } else {
+ $this->return['type'] = 'string';
+ }
+
+ if (isset($docInfo['return']['description'])) {
+ $this->return['description'] = $docInfo['return']['description'];
+ }
+ }
+
+ /**
+ * Parse a doc block
+ *
+ * @param string $doc
+ * @return array
+ */
+ protected function parseDocBlock($doc)
+ {
+ // strip asterisks and leading spaces
+ $doc = preg_replace(
+ ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'],
+ ['', '', '', ''],
+ $doc
+ );
+
+ $doc = trim($doc);
+
+ // get all tags
+ $tags = [];
+ if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $tags[$match[1]][] = trim($match[2]);
+ }
+ }
+ $params = $this->extractDocTags($tags);
+
+ // strip the tags from the doc
+ $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc);
+
+ [$summary, $description] = sexplode("\n\n", $doc, 2, '');
+ return array_merge(
+ [
+ 'summary' => trim($summary),
+ 'description' => trim($description),
+ 'tags' => $tags,
+ ],
+ $params
+ );
+ }
+
+ /**
+ * Process the param and return tags
+ *
+ * @param array $tags
+ * @return array
+ */
+ protected function extractDocTags(&$tags)
+ {
+ $result = [];
+
+ if (isset($tags['param'])) {
+ foreach ($tags['param'] as $param) {
+ if (preg_match('/^(\w+)\s+\$(\w+)(\s+(.*))?$/m', $param, $m)) {
+ $result['args'][$m[2]] = [
+ 'type' => $this->cleanTypeHint($m[1]),
+ 'description' => trim($m[3] ?? ''),
+ ];
+ }
+ }
+ unset($tags['param']);
+ }
+
+
+ if (isset($tags['return'])) {
+ $return = $tags['return'][0];
+ if (preg_match('/^(\w+)(\s+(.*))$/m', $return, $m)) {
+ $result['return'] = [
+ 'type' => $this->cleanTypeHint($m[1]),
+ 'description' => trim($m[2] ?? '')
+ ];
+ }
+ unset($tags['return']);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Matches the given type hint against the valid options for the remote API
+ *
+ * @param string $hint
+ * @return string
+ */
+ protected function cleanTypeHint($hint)
+ {
+ $types = explode('|', $hint);
+ foreach ($types as $t) {
+ if (str_ends_with($t, '[]')) {
+ return 'array';
+ }
+ if ($t === 'boolean' || $t === 'true' || $t === 'false') {
+ return 'bool';
+ }
+ if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) {
+ return $t;
+ }
+ }
+ return 'string';
+ }
+
+ /**
+ * Converts named arguments to positional arguments
+ *
+ * @fixme with PHP 8 we can use named arguments directly using the spread operator
+ * @param array $params
+ * @return array
+ */
+ protected function namedArgsToPositional($params)
+ {
+ $args = [];
+
+ foreach (array_keys($this->args) as $arg) {
+ if (isset($params[$arg])) {
+ $args[] = $params[$arg];
+ } else {
+ $args[] = null;
+ }
+ }
+
+ return $args;
+ }
+
+}
diff --git a/inc/Remote/ApiCore.php b/inc/Remote/ApiCore.php
index 6f0130ba4..e74115663 100644
--- a/inc/Remote/ApiCore.php
+++ b/inc/Remote/ApiCore.php
@@ -9,8 +9,6 @@ use dokuwiki\Extension\AuthPlugin;
use dokuwiki\Extension\Event;
use dokuwiki\Utf8\Sort;
-define('DOKU_API_VERSION', 11);
-
/**
* Provides the core methods for the remote API.
* The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
@@ -40,201 +38,52 @@ class ApiCore
public function getRemoteInfo()
{
return [
- 'dokuwiki.getVersion' => [
- 'args' => [],
- 'return' => 'string',
- 'doc' => 'Returns the running DokuWiki version.'
- ],
- 'dokuwiki.login' => [
- 'args' => ['string', 'string'],
- 'return' => 'int',
- 'doc' => 'Tries to login with the given credentials and sets auth cookies.',
- 'public' => '1'
- ],
- 'dokuwiki.logoff' => [
- 'args' => [],
- 'return' => 'int',
- 'doc' => 'Tries to logoff by expiring auth cookies and the associated PHP session.'
- ],
- 'dokuwiki.getPagelist' => [
- 'args' => ['string', 'array'],
- 'return' => 'array',
- 'doc' => 'List all pages within the given namespace.',
- 'name' => 'readNamespace'
- ],
- 'dokuwiki.search' => [
- 'args' => ['string'],
- 'return' => 'array',
- 'doc' => 'Perform a fulltext search and return a list of matching pages'
- ],
- 'dokuwiki.getTime' => [
- 'args' => [],
- 'return' => 'int',
- 'doc' => 'Returns the current time at the remote wiki server as Unix timestamp.'
- ],
- 'dokuwiki.setLocks' => [
- 'args' => ['array'],
- 'return' => 'array',
- 'doc' => 'Lock or unlock pages.'
- ],
- 'dokuwiki.getTitle' => [
- 'args' => [],
- 'return' => 'string',
- 'doc' => 'Returns the wiki title.',
- 'public' => '1'
- ],
- 'dokuwiki.appendPage' => [
- 'args' => ['string', 'string', 'array'],
- 'return' => 'bool',
- 'doc' => 'Append text to a wiki page.'
- ],
- 'dokuwiki.createUser' => [
- 'args' => ['struct'],
- 'return' => 'bool',
- 'doc' => 'Create a user. The result is boolean'
- ],
- 'dokuwiki.deleteUsers' => [
- 'args' => ['array'],
- 'return' => 'bool',
- 'doc' => 'Remove one or more users from the list of registered users.'
- ],
- 'wiki.getPage' => [
- 'args' => ['string'],
- 'return' => 'string',
- 'doc' => 'Get the raw Wiki text of page, latest version.',
- 'name' => 'rawPage'
- ],
- 'wiki.getPageVersion' => [
- 'args' => ['string', 'int'],
- 'name' => 'rawPage',
- 'return' => 'string',
- 'doc' => 'Return a raw wiki page'
- ],
- 'wiki.getPageHTML' => [
- 'args' => ['string'],
- 'return' => 'string',
- 'doc' => 'Return page in rendered HTML, latest version.',
- 'name' => 'htmlPage'
- ],
- 'wiki.getPageHTMLVersion' => [
- 'args' => ['string', 'int'],
- 'return' => 'string',
- 'doc' => 'Return page in rendered HTML.',
- 'name' => 'htmlPage'
- ],
- 'wiki.getAllPages' => [
- 'args' => [],
- 'return' => 'array',
- 'doc' => 'Returns a list of all pages. The result is an array of utf8 pagenames.',
- 'name' => 'listPages'
- ],
- 'wiki.getAttachments' => [
- 'args' => ['string', 'array'],
- 'return' => 'array',
- 'doc' => 'Returns a list of all media files.',
- 'name' => 'listAttachments'
- ],
- 'wiki.getBackLinks' => [
- 'args' => ['string'],
- 'return' => 'array',
- 'doc' => 'Returns the pages that link to this page.',
- 'name' => 'listBackLinks'
- ],
- 'wiki.getPageInfo' => [
- 'args' => ['string'],
- 'return' => 'array',
- 'doc' => 'Returns a struct with info about the page, latest version.',
- 'name' => 'pageInfo'
- ],
- 'wiki.getPageInfoVersion' => [
- 'args' => ['string', 'int'],
- 'return' => 'array',
- 'doc' => 'Returns a struct with info about the page.',
- 'name' => 'pageInfo'
- ],
- 'wiki.getPageVersions' => [
- 'args' => ['string', 'int'],
- 'return' => 'array',
- 'doc' => 'Returns the available revisions of the page.',
- 'name' => 'pageVersions'
- ],
- 'wiki.putPage' => [
- 'args' => ['string', 'string', 'array'],
- 'return' => 'bool',
- 'doc' => 'Saves a wiki page.'
- ],
- 'wiki.listLinks' => [
- 'args' => ['string'],
- 'return' => 'array',
- 'doc' => 'Lists all links contained in a wiki page.'
- ],
- 'wiki.getRecentChanges' => [
- 'args' => ['int'],
- 'return' => 'array',
- 'doc' => 'Returns a struct about all recent changes since given timestamp.'
- ],
- 'wiki.getRecentMediaChanges' => [
- 'args' => ['int'],
- 'return' => 'array',
- 'doc' => 'Returns a struct about all recent media changes since given timestamp.'
- ],
- 'wiki.aclCheck' => ['args' => ['string', 'string', 'array'],
- 'return' => 'int',
- 'doc' => 'Returns the permissions of a given wiki page. By default, for current user/groups'
- ],
- 'wiki.putAttachment' => ['args' => ['string', 'file', 'array'],
- 'return' => 'array',
- 'doc' => 'Upload a file to the wiki.'
- ],
- 'wiki.deleteAttachment' => [
- 'args' => ['string'],
- 'return' => 'int',
- 'doc' => 'Delete a file from the wiki.'
- ],
- 'wiki.getAttachment' => [
- 'args' => ['string'],
- 'doc' => 'Return a media file',
- 'return' => 'file',
- 'name' => 'getAttachment'
- ],
- 'wiki.getAttachmentInfo' => [
- 'args' => ['string'],
- 'return' => 'array',
- 'doc' => 'Returns a struct with info about the attachment.'
- ],
- 'dokuwiki.getXMLRPCAPIVersion' => [
- 'args' => [],
- 'name' => 'getAPIVersion',
- 'return' => 'int',
- 'doc' => 'Returns the XMLRPC API version.',
- 'public' => '1'
- ],
- 'wiki.getRPCVersionSupported' => [
- 'args' => [],
- 'name' => 'wikiRpcVersion',
- 'return' => 'int',
- 'doc' => 'Returns 2 with the supported RPC API version.',
- 'public' => '1']
+ 'dokuwiki.getVersion' => new ApiCall('getVersion'),
+ 'dokuwiki.login' => (new ApiCall([$this, 'login']))
+ ->setPublic(),
+ 'dokuwiki.logoff' => new ApiCall([$this, 'logoff']),
+ 'dokuwiki.getPagelist' => new ApiCall([$this, 'readNamespace']),
+ 'dokuwiki.search' => new ApiCall([$this, 'search']),
+ 'dokuwiki.getTime' => (new ApiCall('time'))
+ ->setSummary('Returns the current server time')
+ ->setReturnDescription('unix timestamp'),
+ 'dokuwiki.setLocks' => new ApiCall([$this, 'setLocks']),
+ 'dokuwiki.getTitle' => (new ApiCall([$this, 'getTitle']))
+ ->setPublic(),
+ 'dokuwiki.appendPage' => new ApiCall([$this, 'appendPage']),
+ 'dokuwiki.createUser' => new ApiCall([$this, 'createUser']),
+ 'dokuwiki.deleteUsers' => new ApiCall([$this, 'deleteUsers']),
+ 'wiki.getPage' => (new ApiCall([$this, 'rawPage']))
+ ->limitArgs(['id']),
+ 'wiki.getPageVersion' => (new ApiCall([$this, 'rawPage']))
+ ->setSummary('Get a specific revision of a wiki page'),
+ 'wiki.getPageHTML' => (new ApiCall([$this, 'htmlPage']))
+ ->limitArgs(['id']),
+ 'wiki.getPageHTMLVersion' => (new ApiCall([$this, 'htmlPage']))
+ ->setSummary('Get the HTML for a specific revision of a wiki page'),
+ 'wiki.getAllPages' => new ApiCall([$this, 'listPages']),
+ 'wiki.getAttachments' => new ApiCall([$this, 'listAttachments']),
+ 'wiki.getBackLinks' => new ApiCall([$this, 'listBackLinks']),
+ 'wiki.getPageInfo' => (new ApiCall([$this, 'pageInfo']))
+ ->limitArgs(['id']),
+ 'wiki.getPageInfoVersion' => (new ApiCall([$this, 'pageInfo']))
+ ->setSummary('Get some basic data about a specific revison of a wiki page'),
+ 'wiki.getPageVersions' => new ApiCall([$this, 'pageVersions']),
+ 'wiki.putPage' => new ApiCall([$this, 'putPage']),
+ 'wiki.listLinks' => new ApiCall([$this, 'listLinks']),
+ 'wiki.getRecentChanges' => new ApiCall([$this, 'getRecentChanges']),
+ 'wiki.getRecentMediaChanges' => new ApiCall([$this, 'getRecentMediaChanges']),
+ 'wiki.aclCheck' => new ApiCall([$this, 'aclCheck']),
+ 'wiki.putAttachment' => new ApiCall([$this, 'putAttachment']),
+ 'wiki.deleteAttachment' => new ApiCall([$this, 'deleteAttachment']),
+ 'wiki.getAttachment' => new ApiCall([$this, 'getAttachment']),
+ 'wiki.getAttachmentInfo' => new ApiCall([$this, 'getAttachmentInfo']),
+ 'dokuwiki.getXMLRPCAPIVersion' => (new ApiCall([$this, 'getAPIVersion']))->setPublic(),
+ 'wiki.getRPCVersionSupported' => (new ApiCall([$this, 'wikiRpcVersion']))->setPublic(),
];
}
/**
- * @return string
- */
- public function getVersion()
- {
- return getVersion();
- }
-
- /**
- * @return int unix timestamp
- */
- public function getTime()
- {
- return time();
- }
-
- /**
* Return a raw wiki page
*
* @param string $id wiki page id
@@ -987,6 +836,13 @@ class ApiCore
/**
* The version of Wiki RPC API supported
+ *
+ * This is the version of the Wiki RPC specification implemented. Since that specification
+ * is no longer maintained, this will always return 2
+ *
+ * You probably want to look at dokuwiki.getXMLRPCAPIVersion instead
+ *
+ * @return int
*/
public function wikiRpcVersion()
{
diff --git a/inc/Remote/JsonRpcServer.php b/inc/Remote/JsonRpcServer.php
index ff91e0251..ecfc144bf 100644
--- a/inc/Remote/JsonRpcServer.php
+++ b/inc/Remote/JsonRpcServer.php
@@ -169,16 +169,8 @@ class JsonRpcServer
*/
public function call($methodname, $args)
{
- if (!array_is_list($args)) {
- throw new RemoteException(
- "server error. arguments need to passed as list. named arguments not supported",
- -32602
- );
- }
-
try {
- $result = $this->remote->call($methodname, $args);
- return $result;
+ return $this->remote->call($methodname, $args);
} catch (AccessDeniedException $e) {
if (!isset($_SERVER['REMOTE_USER'])) {
http_status(401);