1<?php
2
3namespace dokuwiki\Remote;
4
5use dokuwiki\Extension\RemotePlugin;
6use dokuwiki\Logger;
7use dokuwiki\test\Remote\Mock\ApiCore as MockApiCore;
8
9/**
10 * This class provides information about remote access to the wiki.
11 *
12 * == Types of methods ==
13 * There are two types of remote methods. The first is the core methods.
14 * These are always available and provided by dokuwiki.
15 * The other is plugin methods. These are provided by remote plugins.
16 *
17 * == Information structure ==
18 * The information about methods will be given in an array with the following structure:
19 * array(
20 *     'method.remoteName' => array(
21 *          'args' => array(
22 *              'type eg. string|int|...|date|file',
23 *          )
24 *          'name' => 'method name in class',
25 *          'return' => 'type',
26 *          'public' => 1/0 - method bypass default group check (used by login)
27 *          ['doc' = 'method documentation'],
28 *     )
29 * )
30 *
31 * plugin names are formed the following:
32 *   core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
33 *   i.e.: dokuwiki.version or wiki.getPage
34 *
35 * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
36 * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
37 */
38class Api
39{
40    /** @var ApiCall[] core methods provided by dokuwiki */
41    protected $coreMethods;
42
43    /** @var ApiCall[] remote methods provided by dokuwiki plugins */
44    protected $pluginMethods;
45
46    /**
47     * Get all available methods with remote access.
48     *
49     * @return ApiCall[] with information to all available methods
50     */
51    public function getMethods()
52    {
53        return array_merge($this->getCoreMethods(), $this->getPluginMethods());
54    }
55
56    /**
57     * Collects all the core methods
58     *
59     * @param ApiCore|MockApiCore $apiCore this parameter is used for testing.
60     *        Here you can pass a non-default RemoteAPICore instance. (for mocking)
61     * @return ApiCall[] all core methods.
62     */
63    public function getCoreMethods($apiCore = null)
64    {
65        if (!$this->coreMethods) {
66            if ($apiCore === null) {
67                $this->coreMethods = (new LegacyApiCore())->getMethods();
68            } else {
69                $this->coreMethods = $apiCore->getMethods();
70            }
71        }
72        return $this->coreMethods;
73    }
74
75    /**
76     * Collects all the methods of the enabled Remote Plugins
77     *
78     * @return ApiCall[] all plugin methods.
79     */
80    public function getPluginMethods()
81    {
82        if ($this->pluginMethods) return $this->pluginMethods;
83
84        $plugins = plugin_list('remote');
85        foreach ($plugins as $pluginName) {
86            /** @var RemotePlugin $plugin */
87            $plugin = plugin_load('remote', $pluginName);
88            if (!is_subclass_of($plugin, RemotePlugin::class)) {
89                Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin");
90                continue;
91            }
92
93            try {
94                $methods = $plugin->getMethods();
95            } catch (\ReflectionException $e) {
96                Logger::error(
97                    "Remote Plugin $pluginName failed to return methods",
98                    $e->getMessage(),
99                    $e->getFile(),
100                    $e->getLine()
101                );
102                continue;
103            }
104
105            foreach ($methods as $method => $call) {
106                $this->pluginMethods["plugin.$pluginName.$method"] = $call;
107            }
108        }
109
110        return $this->pluginMethods;
111    }
112
113    /**
114     * Call a method via remote api.
115     *
116     * @param string $method name of the method to call.
117     * @param array $args arguments to pass to the given method
118     * @return mixed result of method call, must be a primitive type.
119     * @throws RemoteException
120     */
121    public function call($method, $args = [])
122    {
123        if ($args === null) {
124            $args = [];
125        }
126
127        // pre-flight checks
128        $this->ensureApiIsEnabled();
129        $methods = $this->getMethods();
130        if (!isset($methods[$method])) {
131            throw new RemoteException('Method does not exist', -32603);
132        }
133        $this->ensureAccessIsAllowed($methods[$method]);
134
135        // invoke the ApiCall
136        try {
137            return $methods[$method]($args);
138        } catch (\InvalidArgumentException | \ArgumentCountError $e) {
139            throw new RemoteException($e->getMessage(), -32602);
140        }
141    }
142
143    /**
144     * Check that the API is generally enabled
145     *
146     * @return void
147     * @throws RemoteException thrown when the API is disabled
148     */
149    public function ensureApiIsEnabled()
150    {
151        global $conf;
152        if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') {
153            throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604);
154        }
155    }
156
157    /**
158     * Check if the current user is allowed to call the given method
159     *
160     * @param ApiCall $method
161     * @return void
162     * @throws AccessDeniedException Thrown when the user is not allowed to call the method
163     */
164    public function ensureAccessIsAllowed(ApiCall $method)
165    {
166        global $conf;
167        global $INPUT;
168        global $USERINFO;
169
170        if ($method->isPublic()) return; // public methods are always allowed
171        if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users
172        if (trim($conf['remoteuser']) === '') return; // all users are allowed
173        if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) {
174            return; // user is allowed
175        }
176
177        // still here? no can do
178        throw new AccessDeniedException('server error. not authorized to call method', -32604);
179    }
180}
181