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.
This commit is contained in:
Andreas Gohr 2023-11-30 19:57:15 +01:00
parent 5ee967131b
commit 42e66c7a3a
6 changed files with 607 additions and 515 deletions

View File

@ -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');
}
}

View File

@ -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
*/

View File

@ -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()
@ -80,6 +65,63 @@ class Api
return array_merge($this->getCoreMethods(), $this->getPluginMethods());
}
/**
* 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 ApiCall[] all core methods.
*/
public function getCoreMethods($apiCore = null)
{
if (!$this->coreMethods) {
if ($apiCore === null) {
$this->coreMethods = (new ApiCore($this))->getRemoteInfo();
} else {
$this->coreMethods = $apiCore->getRemoteInfo();
}
}
return $this->coreMethods;
}
/**
* Collects all the methods of the enabled Remote Plugins
*
* @return ApiCall[] all plugin methods.
*/
public function getPluginMethods()
{
if ($this->pluginMethods) return $this->pluginMethods;
$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;
}
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->pluginMethods;
}
/**
* Call a method via remote api.
*
@ -93,261 +135,59 @@ class Api
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);
}
return $this->callCustomCallPlugin($method, $args);
}
/**
* Check existance of core methods
*
* @param string $name name of the method
* @return bool if method exists
*/
private function coreMethodExist($name)
{
$coreMethods = $this->getCoreMethods();
return array_key_exists($name, $coreMethods);
}
/**
* 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)) {
// pre-flight checks
$this->ensureApiIsEnabled();
$methods = $this->getMethods();
if (!isset($methods[$method])) {
throw new RemoteException('Method does not exist', -32603);
}
[$plugin, $method] = $customCalls[$method];
$fullMethod = "plugin.$plugin.$method";
return $this->callPlugin($plugin, $fullMethod, $args);
}
$this->ensureAccessIsAllowed($methods[$method]);
/**
* 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;
}
return $this->pluginCustomCalls;
}
/**
* Call a plugin method
*
* @param string $pluginName
* @param string $method method name
* @param array $args
* @return mixed return of custom method
* @throws RemoteException
*/
private function callPlugin($pluginName, $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);
// invoke the ApiCall
try {
set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
return call_user_func_array([$plugin, $name], $args);
return $methods[$method]($args);
} catch (\ArgumentCountError $th) {
throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
} finally {
restore_error_handler();
}
}
/**
* 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])) {
throw new RemoteException('Method does not exist', -32603);
}
$this->checkArgumentLength($coreMethods[$method], $args);
try {
set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
return call_user_func_array([$this->coreMethods, $this->getMethodName($coreMethods, $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
*
* @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
*/
public function hasAccess()
{
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;
}
return auth_isMember(
$conf['remoteuser'],
$INPUT->server->str('REMOTE_USER'),
(array)($USERINFO['grps'] ?? [])
);
}
/**
* Requests access
* Check that the API is generally enabled
*
* @return void
* @throws AccessDeniedException On denied access.
* @throws RemoteException thrown when the API is disabled
*/
public function forceAccess()
protected function ensureApiIsEnabled()
{
if (!$this->hasAccess()) {
throw new AccessDeniedException('server error. not authorized to call method', -32604);
global $conf;
if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') {
throw new RemoteException('Server Error. API is not enabled in config.', -32604);
}
}
/**
* Collects all the methods of the enabled Remote Plugins
* Check if the current user is allowed to call the given method
*
* @return array all plugin methods.
* @throws RemoteException if not implemented
* @param ApiCall $method
* @return void
* @throws AccessDeniedException Thrown when the user is not allowed to call the method
*/
public function getPluginMethods()
protected function ensureAccessIsAllowed(ApiCall $method)
{
if ($this->pluginMethods === null) {
$this->pluginMethods = [];
$plugins = plugin_list('remote');
global $conf;
global $INPUT;
global $USERINFO;
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);
}
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);
}
}
}

356
inc/Remote/ApiCall.php Normal file
View File

@ -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;
}
}

View File

@ -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,200 +38,51 @@ 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
*
@ -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()
{

View File

@ -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);