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