code style: line breaks
[dokuwiki.git] / inc / Remote / Api.php
blob99dbf2bd46c9ac38c30d4cda64783d8374642b2f
1 <?php
3 namespace dokuwiki\Remote;
5 use dokuwiki\Extension\PluginInterface;
6 use dokuwiki\Input\Input;
7 use dokuwiki\Extension\Event;
8 use dokuwiki\Extension\RemotePlugin;
10 /**
11 * This class provides information about remote access to the wiki.
13 * == Types of methods ==
14 * There are two types of remote methods. The first is the core methods.
15 * These are always available and provided by dokuwiki.
16 * The other is plugin methods. These are provided by remote plugins.
18 * == Information structure ==
19 * The information about methods will be given in an array with the following structure:
20 * array(
21 * 'method.remoteName' => array(
22 * 'args' => array(
23 * 'type eg. string|int|...|date|file',
24 * )
25 * 'name' => 'method name in class',
26 * 'return' => 'type',
27 * 'public' => 1/0 - method bypass default group check (used by login)
28 * ['doc' = 'method documentation'],
29 * )
30 * )
32 * plugin names are formed the following:
33 * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
34 * i.e.: dokuwiki.version or wiki.getPage
36 * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
37 * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
39 class Api
41 /**
42 * @var ApiCore|\RemoteAPICoreTest
44 private $coreMethods;
46 /**
47 * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
48 * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
50 private $pluginMethods;
52 /**
53 * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
54 * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
56 * The remote method name is the same as in the remote name returned by _getMethods().
58 private $pluginCustomCalls;
60 private $dateTransformation;
61 private $fileTransformation;
63 /**
64 * constructor
66 public function __construct()
68 $this->dateTransformation = [$this, 'dummyTransformation'];
69 $this->fileTransformation = [$this, 'dummyTransformation'];
72 /**
73 * Get all available methods with remote access.
75 * @return array with information to all available methods
76 * @throws RemoteException
78 public function getMethods()
80 return array_merge($this->getCoreMethods(), $this->getPluginMethods());
83 /**
84 * Call a method via remote api.
86 * @param string $method name of the method to call.
87 * @param array $args arguments to pass to the given method
88 * @return mixed result of method call, must be a primitive type.
89 * @throws RemoteException
91 public function call($method, $args = [])
93 if ($args === null) {
94 $args = [];
96 // Ensure we have at least one '.' in $method
97 [$type, $pluginName, /* call */] = sexplode('.', $method . '.', 3, '');
98 if ($type === 'plugin') {
99 return $this->callPlugin($pluginName, $method, $args);
101 if ($this->coreMethodExist($method)) {
102 return $this->callCoreMethod($method, $args);
104 return $this->callCustomCallPlugin($method, $args);
108 * Check existance of core methods
110 * @param string $name name of the method
111 * @return bool if method exists
113 private function coreMethodExist($name)
115 $coreMethods = $this->getCoreMethods();
116 return array_key_exists($name, $coreMethods);
120 * Try to call custom methods provided by plugins
122 * @param string $method name of method
123 * @param array $args
124 * @return mixed
125 * @throws RemoteException if method not exists
127 private function callCustomCallPlugin($method, $args)
129 $customCalls = $this->getCustomCallPlugins();
130 if (!array_key_exists($method, $customCalls)) {
131 throw new RemoteException('Method does not exist', -32603);
133 [$plugin, $method] = $customCalls[$method];
134 $fullMethod = "plugin.$plugin.$method";
135 return $this->callPlugin($plugin, $fullMethod, $args);
139 * Returns plugin calls that are registered via RPC_CALL_ADD action
141 * @return array with pairs of custom plugin calls
142 * @triggers RPC_CALL_ADD
144 private function getCustomCallPlugins()
146 if ($this->pluginCustomCalls === null) {
147 $data = [];
148 Event::createAndTrigger('RPC_CALL_ADD', $data);
149 $this->pluginCustomCalls = $data;
151 return $this->pluginCustomCalls;
155 * Call a plugin method
157 * @param string $pluginName
158 * @param string $method method name
159 * @param array $args
160 * @return mixed return of custom method
161 * @throws RemoteException
163 private function callPlugin($pluginName, $method, $args)
165 $plugin = plugin_load('remote', $pluginName);
166 $methods = $this->getPluginMethods();
167 if (!$plugin instanceof PluginInterface) {
168 throw new RemoteException('Method does not exist', -32603);
170 $this->checkAccess($methods[$method]);
171 $name = $this->getMethodName($methods, $method);
172 try {
173 set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
174 return call_user_func_array([$plugin, $name], $args);
175 } catch (\ArgumentCountError $th) {
176 throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
177 } finally {
178 restore_error_handler();
183 * Call a core method
185 * @param string $method name of method
186 * @param array $args
187 * @return mixed
188 * @throws RemoteException if method not exist
190 private function callCoreMethod($method, $args)
192 $coreMethods = $this->getCoreMethods();
193 $this->checkAccess($coreMethods[$method]);
194 if (!isset($coreMethods[$method])) {
195 throw new RemoteException('Method does not exist', -32603);
197 $this->checkArgumentLength($coreMethods[$method], $args);
198 try {
199 set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
200 return call_user_func_array([$this->coreMethods, $this->getMethodName($coreMethods, $method)], $args);
201 } catch (\ArgumentCountError $th) {
202 throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
203 } finally {
204 restore_error_handler();
209 * Check if access should be checked
211 * @param array $methodMeta data about the method
212 * @throws AccessDeniedException
214 private function checkAccess($methodMeta)
216 if (!isset($methodMeta['public'])) {
217 $this->forceAccess();
218 } elseif ($methodMeta['public'] == '0') {
219 $this->forceAccess();
224 * Check the number of parameters
226 * @param array $methodMeta data about the method
227 * @param array $args
228 * @throws RemoteException if wrong parameter count
230 private function checkArgumentLength($methodMeta, $args)
232 if (count($methodMeta['args']) < count($args)) {
233 throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
238 * Determine the name of the real method
240 * @param array $methodMeta list of data of the methods
241 * @param string $method name of method
242 * @return string
244 private function getMethodName($methodMeta, $method)
246 if (isset($methodMeta[$method]['name'])) {
247 return $methodMeta[$method]['name'];
249 $method = explode('.', $method);
250 return $method[count($method) - 1];
254 * Perform access check for current user
256 * @return bool true if the current user has access to remote api.
257 * @throws AccessDeniedException If remote access disabled
259 public function hasAccess()
261 global $conf;
262 global $USERINFO;
263 /** @var Input $INPUT */
264 global $INPUT;
266 if (!$conf['remote']) {
267 throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
269 if (trim($conf['remoteuser']) == '!!not set!!') {
270 return false;
272 if (!$conf['useacl']) {
273 return true;
275 if (trim($conf['remoteuser']) == '') {
276 return true;
279 return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
283 * Requests access
285 * @return void
286 * @throws AccessDeniedException On denied access.
288 public function forceAccess()
290 if (!$this->hasAccess()) {
291 throw new AccessDeniedException('server error. not authorized to call method', -32604);
296 * Collects all the methods of the enabled Remote Plugins
298 * @return array all plugin methods.
299 * @throws RemoteException if not implemented
301 public function getPluginMethods()
303 if ($this->pluginMethods === null) {
304 $this->pluginMethods = [];
305 $plugins = plugin_list('remote');
307 foreach ($plugins as $pluginName) {
308 /** @var RemotePlugin $plugin */
309 $plugin = plugin_load('remote', $pluginName);
310 if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
311 throw new RemoteException(
312 "Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"
316 try {
317 $methods = $plugin->_getMethods();
318 } catch (\ReflectionException $e) {
319 throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
322 foreach ($methods as $method => $meta) {
323 $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
327 return $this->pluginMethods;
331 * Collects all the core methods
333 * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing.
334 * Here you can pass a non-default RemoteAPICore instance. (for mocking)
335 * @return array all core methods.
337 public function getCoreMethods($apiCore = null)
339 if ($this->coreMethods === null) {
340 if ($apiCore === null) {
341 $this->coreMethods = new ApiCore($this);
342 } else {
343 $this->coreMethods = $apiCore;
346 return $this->coreMethods->getRemoteInfo();
350 * Transform file to xml
352 * @param mixed $data
353 * @return mixed
355 public function toFile($data)
357 return call_user_func($this->fileTransformation, $data);
361 * Transform date to xml
363 * @param mixed $data
364 * @return mixed
366 public function toDate($data)
368 return call_user_func($this->dateTransformation, $data);
372 * A simple transformation
374 * @param mixed $data
375 * @return mixed
377 public function dummyTransformation($data)
379 return $data;
383 * Set the transformer function
385 * @param callback $dateTransformation
387 public function setDateTransformation($dateTransformation)
389 $this->dateTransformation = $dateTransformation;
393 * Set the transformer function
395 * @param callback $fileTransformation
397 public function setFileTransformation($fileTransformation)
399 $this->fileTransformation = $fileTransformation;
403 * The error handler that catches argument-related warnings
405 public function argumentWarningHandler($errno, $errstr)
407 if (substr($errstr, 0, 17) == 'Missing argument ') {
408 throw new RemoteException('Method does not exist - wrong parameter count.', -32603);