xref: /dokuwiki/inc/Remote/Api.php (revision 256ac28f4fc061dc35155f13d1f6d174278a204e)
1<?php
2
3namespace dokuwiki\Remote;
4
5use dokuwiki\Extension\Event;
6use dokuwiki\Extension\RemotePlugin;
7
8/**
9 * This class provides information about remote access to the wiki.
10 *
11 * == Types of methods ==
12 * There are two types of remote methods. The first is the core methods.
13 * These are always available and provided by dokuwiki.
14 * The other is plugin methods. These are provided by remote plugins.
15 *
16 * == Information structure ==
17 * The information about methods will be given in an array with the following structure:
18 * array(
19 *     'method.remoteName' => array(
20 *          'args' => array(
21 *              'type eg. string|int|...|date|file',
22 *          )
23 *          'name' => 'method name in class',
24 *          'return' => 'type',
25 *          'public' => 1/0 - method bypass default group check (used by login)
26 *          ['doc' = 'method documentation'],
27 *     )
28 * )
29 *
30 * plugin names are formed the following:
31 *   core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
32 *   i.e.: dokuwiki.version or wiki.getPage
33 *
34 * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
35 * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
36 */
37class Api
38{
39
40    /**
41     * @var ApiCore
42     */
43    private $coreMethods = null;
44
45    /**
46     * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
47     * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
48     */
49    private $pluginMethods = null;
50
51    /**
52     * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
53     * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
54     *
55     * The remote method name is the same as in the remote name returned by _getMethods().
56     */
57    private $pluginCustomCalls = null;
58
59    private $dateTransformation;
60    private $fileTransformation;
61
62    /**
63     * constructor
64     */
65    public function __construct()
66    {
67        $this->dateTransformation = array($this, 'dummyTransformation');
68        $this->fileTransformation = array($this, 'dummyTransformation');
69    }
70
71    /**
72     * Get all available methods with remote access.
73     *
74     * @return array with information to all available methods
75     * @throws RemoteException
76     */
77    public function getMethods()
78    {
79        return array_merge($this->getCoreMethods(), $this->getPluginMethods());
80    }
81
82    /**
83     * Call a method via remote api.
84     *
85     * @param string $method name of the method to call.
86     * @param array $args arguments to pass to the given method
87     * @return mixed result of method call, must be a primitive type.
88     * @throws RemoteException
89     */
90    public function call($method, $args = array())
91    {
92        if ($args === null) {
93            $args = array();
94        }
95        list($type, $pluginName, /* $call */) = explode('.', $method, 3);
96        if ($type === 'plugin') {
97            return $this->callPlugin($pluginName, $method, $args);
98        }
99        if ($this->coreMethodExist($method)) {
100            return $this->callCoreMethod($method, $args);
101        }
102        return $this->callCustomCallPlugin($method, $args);
103    }
104
105    /**
106     * Check existance of core methods
107     *
108     * @param string $name name of the method
109     * @return bool if method exists
110     */
111    private function coreMethodExist($name)
112    {
113        $coreMethods = $this->getCoreMethods();
114        return array_key_exists($name, $coreMethods);
115    }
116
117    /**
118     * Try to call custom methods provided by plugins
119     *
120     * @param string $method name of method
121     * @param array $args
122     * @return mixed
123     * @throws RemoteException if method not exists
124     */
125    private function callCustomCallPlugin($method, $args)
126    {
127        $customCalls = $this->getCustomCallPlugins();
128        if (!array_key_exists($method, $customCalls)) {
129            throw new RemoteException('Method does not exist', -32603);
130        }
131        $customCall = $customCalls[$method];
132        return $this->callPlugin($customCall[0], $customCall[1], $args);
133    }
134
135    /**
136     * Returns plugin calls that are registered via RPC_CALL_ADD action
137     *
138     * @return array with pairs of custom plugin calls
139     * @triggers RPC_CALL_ADD
140     */
141    private function getCustomCallPlugins()
142    {
143        if ($this->pluginCustomCalls === null) {
144            $data = array();
145            Event::createAndTrigger('RPC_CALL_ADD', $data);
146            $this->pluginCustomCalls = $data;
147        }
148        return $this->pluginCustomCalls;
149    }
150
151    /**
152     * Call a plugin method
153     *
154     * @param string $pluginName
155     * @param string $method method name
156     * @param array $args
157     * @return mixed return of custom method
158     * @throws RemoteException
159     */
160    private function callPlugin($pluginName, $method, $args)
161    {
162        $plugin = plugin_load('remote', $pluginName);
163        $methods = $this->getPluginMethods();
164        if (!$plugin) {
165            throw new RemoteException('Method does not exist', -32603);
166        }
167        $this->checkAccess($methods[$method]);
168        $name = $this->getMethodName($methods, $method);
169        try {
170            set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
171            return call_user_func_array(array($plugin, $name), $args);
172        } catch (\ArgumentCountError $th) {
173            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
174        } finally {
175            restore_error_handler();
176        }
177    }
178
179    /**
180     * Call a core method
181     *
182     * @param string $method name of method
183     * @param array $args
184     * @return mixed
185     * @throws RemoteException if method not exist
186     */
187    private function callCoreMethod($method, $args)
188    {
189        $coreMethods = $this->getCoreMethods();
190        $this->checkAccess($coreMethods[$method]);
191        if (!isset($coreMethods[$method])) {
192            throw new RemoteException('Method does not exist', -32603);
193        }
194        $this->checkArgumentLength($coreMethods[$method], $args);
195        try {
196            set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
197            return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args);
198        } catch (\ArgumentCountError $th) {
199            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
200        } finally {
201            restore_error_handler();
202        }
203    }
204
205    /**
206     * Check if access should be checked
207     *
208     * @param array $methodMeta data about the method
209     * @throws AccessDeniedException
210     */
211    private function checkAccess($methodMeta)
212    {
213        if (!isset($methodMeta['public'])) {
214            $this->forceAccess();
215        } else {
216            if ($methodMeta['public'] == '0') {
217                $this->forceAccess();
218            }
219        }
220    }
221
222    /**
223     * Check the number of parameters
224     *
225     * @param array $methodMeta data about the method
226     * @param array $args
227     * @throws RemoteException if wrong parameter count
228     */
229    private function checkArgumentLength($methodMeta, $args)
230    {
231        if (count($methodMeta['args']) < count($args)) {
232            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
233        }
234    }
235
236    /**
237     * Determine the name of the real method
238     *
239     * @param array $methodMeta list of data of the methods
240     * @param string $method name of method
241     * @return string
242     */
243    private function getMethodName($methodMeta, $method)
244    {
245        if (isset($methodMeta[$method]['name'])) {
246            return $methodMeta[$method]['name'];
247        }
248        $method = explode('.', $method);
249        return $method[count($method) - 1];
250    }
251
252    /**
253     * Perform access check for current user
254     *
255     * @return bool true if the current user has access to remote api.
256     * @throws AccessDeniedException If remote access disabled
257     */
258    public function hasAccess()
259    {
260        global $conf;
261        global $USERINFO;
262        /** @var \dokuwiki\Input\Input $INPUT */
263        global $INPUT;
264
265        if (!$conf['remote']) {
266            throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
267        }
268        if (trim($conf['remoteuser']) == '!!not set!!') {
269            return false;
270        }
271        if (!$conf['useacl']) {
272            return true;
273        }
274        if (trim($conf['remoteuser']) == '') {
275            return true;
276        }
277
278        return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
279    }
280
281    /**
282     * Requests access
283     *
284     * @return void
285     * @throws AccessDeniedException On denied access.
286     */
287    public function forceAccess()
288    {
289        if (!$this->hasAccess()) {
290            throw new AccessDeniedException('server error. not authorized to call method', -32604);
291        }
292    }
293
294    /**
295     * Collects all the methods of the enabled Remote Plugins
296     *
297     * @return array all plugin methods.
298     * @throws RemoteException if not implemented
299     */
300    public function getPluginMethods()
301    {
302        if ($this->pluginMethods === null) {
303            $this->pluginMethods = array();
304            $plugins = plugin_list('remote');
305
306            foreach ($plugins as $pluginName) {
307                /** @var RemotePlugin $plugin */
308                $plugin = plugin_load('remote', $pluginName);
309                if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
310                    throw new RemoteException(
311                        "Plugin $pluginName does not implement dokuwiki\Plugin\DokuWiki_Remote_Plugin"
312                    );
313                }
314
315                try {
316                    $methods = $plugin->_getMethods();
317                } catch (\ReflectionException $e) {
318                    throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
319                }
320
321                foreach ($methods as $method => $meta) {
322                    $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
323                }
324            }
325        }
326        return $this->pluginMethods;
327    }
328
329    /**
330     * Collects all the core methods
331     *
332     * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore
333     *                         instance. (for mocking)
334     * @return array all core methods.
335     */
336    public function getCoreMethods($apiCore = null)
337    {
338        if ($this->coreMethods === null) {
339            if ($apiCore === null) {
340                $this->coreMethods = new ApiCore($this);
341            } else {
342                $this->coreMethods = $apiCore;
343            }
344        }
345        return $this->coreMethods->__getRemoteInfo();
346    }
347
348    /**
349     * Transform file to xml
350     *
351     * @param mixed $data
352     * @return mixed
353     */
354    public function toFile($data)
355    {
356        return call_user_func($this->fileTransformation, $data);
357    }
358
359    /**
360     * Transform date to xml
361     *
362     * @param mixed $data
363     * @return mixed
364     */
365    public function toDate($data)
366    {
367        return call_user_func($this->dateTransformation, $data);
368    }
369
370    /**
371     * A simple transformation
372     *
373     * @param mixed $data
374     * @return mixed
375     */
376    public function dummyTransformation($data)
377    {
378        return $data;
379    }
380
381    /**
382     * Set the transformer function
383     *
384     * @param callback $dateTransformation
385     */
386    public function setDateTransformation($dateTransformation)
387    {
388        $this->dateTransformation = $dateTransformation;
389    }
390
391    /**
392     * Set the transformer function
393     *
394     * @param callback $fileTransformation
395     */
396    public function setFileTransformation($fileTransformation)
397    {
398        $this->fileTransformation = $fileTransformation;
399    }
400
401    /**
402     * The error handler that catches argument-related warnings
403     */
404    public function argumentWarningHandler($errno, $errstr)
405    {
406        if (substr($errstr, 0, 17) == 'Missing argument ') {
407            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
408        }
409    }
410}
411