array( * 'args' => array( * 'type eg. string|int|...|date|file', * ) * 'name' => 'method name in class', * 'return' => 'type', * 'public' => 1/0 - method bypass default group check (used by login) * ['doc' = 'method documentation'], * ) * ) * * plugin names are formed the following: * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself. * i.e.: dokuwiki.version or wiki.getPage * * plugin methods are formed like 'plugin..'. * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime */ class Api { /** * @var ApiCore */ private $coreMethods = null; /** * @var array remote methods provided by dokuwiki plugins - will be filled lazy via * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods} */ private $pluginMethods = null; /** * @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 = null; private $dateTransformation; private $fileTransformation; /** * constructor */ public function __construct() { $this->dateTransformation = array($this, 'dummyTransformation'); $this->fileTransformation = array($this, 'dummyTransformation'); } /** * Get all available methods with remote access. * * @return array with information to all available methods * @throws RemoteException */ public function getMethods() { return array_merge($this->getCoreMethods(), $this->getPluginMethods()); } /** * Call a method via remote api. * * @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 */ public function call($method, $args = array()) { if ($args === null) { $args = array(); } // Ensure we have at least one '.' in $method list($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)) { throw new RemoteException('Method does not exist', -32603); } list($plugin, $method) = $customCalls[$method]; $fullMethod = "plugin.$plugin.$method"; return $this->callPlugin($plugin, $fullMethod, $args); } /** * 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 = array(); 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) { throw new RemoteException('Method does not exist', -32603); } $this->checkAccess($methods[$method]); $name = $this->getMethodName($methods, $method); try { set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1 return call_user_func_array(array($plugin, $name), $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(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1 return call_user_func_array(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(); } else { if ($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 \dokuwiki\Input\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 * * @return void * @throws AccessDeniedException On denied access. */ public function forceAccess() { 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 = array(); $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\Plugin\DokuWiki_Remote_Plugin" ); } 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; } } } return $this->pluginMethods; } /** * Collects all the core methods * * @param ApiCore $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(); } /** * Transform file to xml * * @param mixed $data * @return mixed */ public function toFile($data) { return call_user_func($this->fileTransformation, $data); } /** * Transform date to xml * * @param mixed $data * @return mixed */ public function toDate($data) { return call_user_func($this->dateTransformation, $data); } /** * A simple transformation * * @param mixed $data * @return mixed */ public function dummyTransformation($data) { return $data; } /** * Set the transformer function * * @param callback $dateTransformation */ public function setDateTransformation($dateTransformation) { $this->dateTransformation = $dateTransformation; } /** * Set the transformer function * * @param callback $fileTransformation */ public function setFileTransformation($fileTransformation) { $this->fileTransformation = $fileTransformation; } /** * The error handler that catches argument-related warnings */ public function argumentWarningHandler($errno, $errstr) { if (substr($errstr, 0, 17) == 'Missing argument ') { throw new RemoteException('Method does not exist - wrong parameter count.', -32603); } } }