xref: /dokuwiki/inc/Remote/Api.php (revision 012c7cdc1eaa60ee12aedc0a77226245a4e8e121)
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        // Ensure we have at least one '.' in $method
96        list($type, $pluginName, /* $call */) = explode('.', $method . '.', 3);
97        if ($type === 'plugin') {
98            return $this->callPlugin($pluginName, $method, $args);
99        }
100        if ($this->coreMethodExist($method)) {
101            return $this->callCoreMethod($method, $args);
102        }
103        return $this->callCustomCallPlugin($method, $args);
104    }
105
106    /**
107     * Check existance of core methods
108     *
109     * @param string $name name of the method
110     * @return bool if method exists
111     */
112    private function coreMethodExist($name)
113    {
114        $coreMethods = $this->getCoreMethods();
115        return array_key_exists($name, $coreMethods);
116    }
117
118    /**
119     * Try to call custom methods provided by plugins
120     *
121     * @param string $method name of method
122     * @param array $args
123     * @return mixed
124     * @throws RemoteException if method not exists
125     */
126    private function callCustomCallPlugin($method, $args)
127    {
128        $customCalls = $this->getCustomCallPlugins();
129        if (!array_key_exists($method, $customCalls)) {
130            throw new RemoteException('Method does not exist', -32603);
131        }
132        $customCall = $customCalls[$method];
133        return $this->callPlugin($customCall[0], $customCall[1], $args);
134    }
135
136    /**
137     * Returns plugin calls that are registered via RPC_CALL_ADD action
138     *
139     * @return array with pairs of custom plugin calls
140     * @triggers RPC_CALL_ADD
141     */
142    private function getCustomCallPlugins()
143    {
144        if ($this->pluginCustomCalls === null) {
145            $data = array();
146            Event::createAndTrigger('RPC_CALL_ADD', $data);
147            $this->pluginCustomCalls = $data;
148        }
149        return $this->pluginCustomCalls;
150    }
151
152    /**
153     * Call a plugin method
154     *
155     * @param string $pluginName
156     * @param string $method method name
157     * @param array $args
158     * @return mixed return of custom method
159     * @throws RemoteException
160     */
161    private function callPlugin($pluginName, $method, $args)
162    {
163        $plugin = plugin_load('remote', $pluginName);
164        $methods = $this->getPluginMethods();
165        if (!$plugin) {
166            throw new RemoteException('Method does not exist', -32603);
167        }
168        $this->checkAccess($methods[$method]);
169        $name = $this->getMethodName($methods, $method);
170        try {
171            set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
172            return call_user_func_array(array($plugin, $name), $args);
173        } catch (\ArgumentCountError $th) {
174            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
175        } finally {
176            restore_error_handler();
177        }
178    }
179
180    /**
181     * Call a core method
182     *
183     * @param string $method name of method
184     * @param array $args
185     * @return mixed
186     * @throws RemoteException if method not exist
187     */
188    private function callCoreMethod($method, $args)
189    {
190        $coreMethods = $this->getCoreMethods();
191        $this->checkAccess($coreMethods[$method]);
192        if (!isset($coreMethods[$method])) {
193            throw new RemoteException('Method does not exist', -32603);
194        }
195        $this->checkArgumentLength($coreMethods[$method], $args);
196        try {
197            set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
198            return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args);
199        } catch (\ArgumentCountError $th) {
200            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
201        } finally {
202            restore_error_handler();
203        }
204    }
205
206    /**
207     * Check if access should be checked
208     *
209     * @param array $methodMeta data about the method
210     * @throws AccessDeniedException
211     */
212    private function checkAccess($methodMeta)
213    {
214        if (!isset($methodMeta['public'])) {
215            $this->forceAccess();
216        } else {
217            if ($methodMeta['public'] == '0') {
218                $this->forceAccess();
219            }
220        }
221    }
222
223    /**
224     * Check the number of parameters
225     *
226     * @param array $methodMeta data about the method
227     * @param array $args
228     * @throws RemoteException if wrong parameter count
229     */
230    private function checkArgumentLength($methodMeta, $args)
231    {
232        if (count($methodMeta['args']) < count($args)) {
233            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
234        }
235    }
236
237    /**
238     * Determine the name of the real method
239     *
240     * @param array $methodMeta list of data of the methods
241     * @param string $method name of method
242     * @return string
243     */
244    private function getMethodName($methodMeta, $method)
245    {
246        if (isset($methodMeta[$method]['name'])) {
247            return $methodMeta[$method]['name'];
248        }
249        $method = explode('.', $method);
250        return $method[count($method) - 1];
251    }
252
253    /**
254     * Perform access check for current user
255     *
256     * @return bool true if the current user has access to remote api.
257     * @throws AccessDeniedException If remote access disabled
258     */
259    public function hasAccess()
260    {
261        global $conf;
262        global $USERINFO;
263        /** @var \dokuwiki\Input\Input $INPUT */
264        global $INPUT;
265
266        if (!$conf['remote']) {
267            throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
268        }
269        if (trim($conf['remoteuser']) == '!!not set!!') {
270            return false;
271        }
272        if (!$conf['useacl']) {
273            return true;
274        }
275        if (trim($conf['remoteuser']) == '') {
276            return true;
277        }
278
279        return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
280    }
281
282    /**
283     * Requests access
284     *
285     * @return void
286     * @throws AccessDeniedException On denied access.
287     */
288    public function forceAccess()
289    {
290        if (!$this->hasAccess()) {
291            throw new AccessDeniedException('server error. not authorized to call method', -32604);
292        }
293    }
294
295    /**
296     * Collects all the methods of the enabled Remote Plugins
297     *
298     * @return array all plugin methods.
299     * @throws RemoteException if not implemented
300     */
301    public function getPluginMethods()
302    {
303        if ($this->pluginMethods === null) {
304            $this->pluginMethods = array();
305            $plugins = plugin_list('remote');
306
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\Plugin\DokuWiki_Remote_Plugin"
313                    );
314                }
315
316                try {
317                    $methods = $plugin->_getMethods();
318                } catch (\ReflectionException $e) {
319                    throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
320                }
321
322                foreach ($methods as $method => $meta) {
323                    $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
324                }
325            }
326        }
327        return $this->pluginMethods;
328    }
329
330    /**
331     * Collects all the core methods
332     *
333     * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore
334     *                         instance. (for mocking)
335     * @return array all core methods.
336     */
337    public function getCoreMethods($apiCore = null)
338    {
339        if ($this->coreMethods === null) {
340            if ($apiCore === null) {
341                $this->coreMethods = new ApiCore($this);
342            } else {
343                $this->coreMethods = $apiCore;
344            }
345        }
346        return $this->coreMethods->__getRemoteInfo();
347    }
348
349    /**
350     * Transform file to xml
351     *
352     * @param mixed $data
353     * @return mixed
354     */
355    public function toFile($data)
356    {
357        return call_user_func($this->fileTransformation, $data);
358    }
359
360    /**
361     * Transform date to xml
362     *
363     * @param mixed $data
364     * @return mixed
365     */
366    public function toDate($data)
367    {
368        return call_user_func($this->dateTransformation, $data);
369    }
370
371    /**
372     * A simple transformation
373     *
374     * @param mixed $data
375     * @return mixed
376     */
377    public function dummyTransformation($data)
378    {
379        return $data;
380    }
381
382    /**
383     * Set the transformer function
384     *
385     * @param callback $dateTransformation
386     */
387    public function setDateTransformation($dateTransformation)
388    {
389        $this->dateTransformation = $dateTransformation;
390    }
391
392    /**
393     * Set the transformer function
394     *
395     * @param callback $fileTransformation
396     */
397    public function setFileTransformation($fileTransformation)
398    {
399        $this->fileTransformation = $fileTransformation;
400    }
401
402    /**
403     * The error handler that catches argument-related warnings
404     */
405    public function argumentWarningHandler($errno, $errstr)
406    {
407        if (substr($errstr, 0, 17) == 'Missing argument ') {
408            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
409        }
410    }
411}
412