diff options
-rw-r--r-- | _test/tests/Remote/ApiCallTest.php | 98 | ||||
-rw-r--r-- | inc/Extension/RemotePlugin.php | 65 | ||||
-rw-r--r-- | inc/Remote/Api.php | 333 | ||||
-rw-r--r-- | inc/Remote/ApiCall.php | 356 | ||||
-rw-r--r-- | inc/Remote/ApiCore.php | 242 | ||||
-rw-r--r-- | inc/Remote/JsonRpcServer.php | 10 |
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 750220c59..0708eb96f 100644 --- a/inc/Remote/JsonRpcServer.php +++ b/inc/Remote/JsonRpcServer.php @@ -168,16 +168,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); |