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