xref: /dokuwiki/inc/Remote/Api.php (revision 0c6e917818109b1f50cd46754bb3bf353e36d889)
1<?php
2
3namespace dokuwiki\Remote;
4
5use dokuwiki\Extension\RemotePlugin;
6use dokuwiki\Logger;
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    /** @var ApiCall[] core methods provided by dokuwiki */
40    protected $coreMethods;
41
42    /** @var ApiCall[] remote methods provided by dokuwiki plugins */
43    protected $pluginMethods;
44
45    private $dateTransformation;
46    private $fileTransformation;
47
48    /**
49     * constructor
50     */
51    public function __construct()
52    {
53        $this->dateTransformation = [$this, 'dummyTransformation'];
54        $this->fileTransformation = [$this, 'dummyTransformation'];
55    }
56
57    /**
58     * Get all available methods with remote access.
59     *
60     * @return ApiCall[] with information to all available methods
61     * @throws RemoteException
62     */
63    public function getMethods()
64    {
65        return array_merge($this->getCoreMethods(), $this->getPluginMethods());
66    }
67
68    /**
69     * Collects all the core methods
70     *
71     * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing.
72     *        Here you can pass a non-default RemoteAPICore instance. (for mocking)
73     * @return ApiCall[] all core methods.
74     */
75    public function getCoreMethods($apiCore = null)
76    {
77        if (!$this->coreMethods) {
78            if ($apiCore === null) {
79                $this->coreMethods = (new ApiCore($this))->getRemoteInfo();
80            } else {
81                $this->coreMethods = $apiCore->getRemoteInfo();
82            }
83        }
84        return $this->coreMethods;
85    }
86
87    /**
88     * Collects all the methods of the enabled Remote Plugins
89     *
90     * @return ApiCall[] all plugin methods.
91     */
92    public function getPluginMethods()
93    {
94        if ($this->pluginMethods) return $this->pluginMethods;
95
96        $plugins = plugin_list('remote');
97        foreach ($plugins as $pluginName) {
98            /** @var RemotePlugin $plugin */
99            $plugin = plugin_load('remote', $pluginName);
100            if (!is_subclass_of($plugin, RemotePlugin::class)) {
101                Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin");
102                continue;
103            }
104
105            try {
106                $methods = $plugin->getMethods();
107            } catch (\ReflectionException $e) {
108                Logger::error(
109                    "Remote Plugin $pluginName failed to return methods",
110                    $e->getMessage(),
111                    $e->getFile(),
112                    $e->getLine()
113                );
114                continue;
115            }
116
117            foreach ($methods as $method => $call) {
118                $this->pluginMethods["plugin.$pluginName.$method"] = $call;
119            }
120        }
121
122        return $this->pluginMethods;
123    }
124
125    /**
126     * Call a method via remote api.
127     *
128     * @param string $method name of the method to call.
129     * @param array $args arguments to pass to the given method
130     * @return mixed result of method call, must be a primitive type.
131     * @throws RemoteException
132     */
133    public function call($method, $args = [])
134    {
135        if ($args === null) {
136            $args = [];
137        }
138
139        // pre-flight checks
140        $this->ensureApiIsEnabled();
141        $methods = $this->getMethods();
142        if (!isset($methods[$method])) {
143            throw new RemoteException('Method does not exist', -32603);
144        }
145        $this->ensureAccessIsAllowed($methods[$method]);
146
147        // invoke the ApiCall
148        try {
149            return $methods[$method]($args);
150        } catch (\ArgumentCountError $th) {
151            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
152        }
153    }
154
155    /**
156     * Check that the API is generally enabled
157     *
158     * @return void
159     * @throws RemoteException thrown when the API is disabled
160     */
161    public function ensureApiIsEnabled()
162    {
163        global $conf;
164        if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') {
165            throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604);
166        }
167    }
168
169    /**
170     * Check if the current user is allowed to call the given method
171     *
172     * @param ApiCall $method
173     * @return void
174     * @throws AccessDeniedException Thrown when the user is not allowed to call the method
175     */
176    public function ensureAccessIsAllowed(ApiCall $method)
177    {
178        global $conf;
179        global $INPUT;
180        global $USERINFO;
181
182        if ($method->isPublic()) return; // public methods are always allowed
183        if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users
184        if (trim($conf['remoteuser']) === '') return; // all users are allowed
185        if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) {
186            return; // user is allowed
187        }
188
189        // still here? no can do
190        throw new AccessDeniedException('server error. not authorized to call method', -32604);
191    }
192
193    /**
194     * Transform file to xml
195     *
196     * @param mixed $data
197     * @return mixed
198     */
199    public function toFile($data)
200    {
201        return call_user_func($this->fileTransformation, $data);
202    }
203
204    /**
205     * Transform date to xml
206     *
207     * @param mixed $data
208     * @return mixed
209     */
210    public function toDate($data)
211    {
212        return call_user_func($this->dateTransformation, $data);
213    }
214
215    /**
216     * A simple transformation
217     *
218     * @param mixed $data
219     * @return mixed
220     */
221    public function dummyTransformation($data)
222    {
223        return $data;
224    }
225
226    /**
227     * Set the transformer function
228     *
229     * @param callback $dateTransformation
230     */
231    public function setDateTransformation($dateTransformation)
232    {
233        $this->dateTransformation = $dateTransformation;
234    }
235
236    /**
237     * Set the transformer function
238     *
239     * @param callback $fileTransformation
240     */
241    public function setFileTransformation($fileTransformation)
242    {
243        $this->fileTransformation = $fileTransformation;
244    }
245
246}
247