aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-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 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);