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