xref: /dokuwiki/inc/Remote/Api.php (revision 12b99753267c30cd2730a96084b519295d521162)
1dd87735dSAndreas Gohr<?php
2dd87735dSAndreas Gohr
3dd87735dSAndreas Gohrnamespace dokuwiki\Remote;
4dd87735dSAndreas Gohr
5e1d9dcc8SAndreas Gohruse dokuwiki\Extension\RemotePlugin;
642e66c7aSAndreas Gohruse dokuwiki\Logger;
7*12b99753SAndreas Gohruse dokuwiki\test\Remote\Mock\ApiCore as MockApiCore;
8dd87735dSAndreas Gohr
9dd87735dSAndreas Gohr/**
10dd87735dSAndreas Gohr * This class provides information about remote access to the wiki.
11dd87735dSAndreas Gohr *
12dd87735dSAndreas Gohr * == Types of methods ==
13dd87735dSAndreas Gohr * There are two types of remote methods. The first is the core methods.
14dd87735dSAndreas Gohr * These are always available and provided by dokuwiki.
15dd87735dSAndreas Gohr * The other is plugin methods. These are provided by remote plugins.
16dd87735dSAndreas Gohr *
17dd87735dSAndreas Gohr * == Information structure ==
18dd87735dSAndreas Gohr * The information about methods will be given in an array with the following structure:
19dd87735dSAndreas Gohr * array(
20dd87735dSAndreas Gohr *     'method.remoteName' => array(
21dd87735dSAndreas Gohr *          'args' => array(
22dd87735dSAndreas Gohr *              'type eg. string|int|...|date|file',
23dd87735dSAndreas Gohr *          )
24dd87735dSAndreas Gohr *          'name' => 'method name in class',
25dd87735dSAndreas Gohr *          'return' => 'type',
26dd87735dSAndreas Gohr *          'public' => 1/0 - method bypass default group check (used by login)
27dd87735dSAndreas Gohr *          ['doc' = 'method documentation'],
28dd87735dSAndreas Gohr *     )
29dd87735dSAndreas Gohr * )
30dd87735dSAndreas Gohr *
31dd87735dSAndreas Gohr * plugin names are formed the following:
32dd87735dSAndreas Gohr *   core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
33dd87735dSAndreas Gohr *   i.e.: dokuwiki.version or wiki.getPage
34dd87735dSAndreas Gohr *
35dd87735dSAndreas Gohr * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
36dd87735dSAndreas Gohr * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
37dd87735dSAndreas Gohr */
38dd87735dSAndreas Gohrclass Api
39dd87735dSAndreas Gohr{
4042e66c7aSAndreas Gohr    /** @var ApiCall[] core methods provided by dokuwiki */
4142e66c7aSAndreas Gohr    protected $coreMethods;
42dd87735dSAndreas Gohr
4342e66c7aSAndreas Gohr    /** @var ApiCall[] remote methods provided by dokuwiki plugins */
4442e66c7aSAndreas Gohr    protected $pluginMethods;
45dd87735dSAndreas Gohr
46dd87735dSAndreas Gohr    /**
47dd87735dSAndreas Gohr     * Get all available methods with remote access.
48dd87735dSAndreas Gohr     *
4942e66c7aSAndreas Gohr     * @return ApiCall[] with information to all available methods
50dd87735dSAndreas Gohr     */
51dd87735dSAndreas Gohr    public function getMethods()
52dd87735dSAndreas Gohr    {
53dd87735dSAndreas Gohr        return array_merge($this->getCoreMethods(), $this->getPluginMethods());
54dd87735dSAndreas Gohr    }
55dd87735dSAndreas Gohr
56dd87735dSAndreas Gohr    /**
5742e66c7aSAndreas Gohr     * Collects all the core methods
5842e66c7aSAndreas Gohr     *
59b433b69eSAndreas Gohr     * @param ApiCore|MockApiCore $apiCore this parameter is used for testing.
6042e66c7aSAndreas Gohr     *        Here you can pass a non-default RemoteAPICore instance. (for mocking)
6142e66c7aSAndreas Gohr     * @return ApiCall[] all core methods.
6242e66c7aSAndreas Gohr     */
6342e66c7aSAndreas Gohr    public function getCoreMethods($apiCore = null)
6442e66c7aSAndreas Gohr    {
6542e66c7aSAndreas Gohr        if (!$this->coreMethods) {
6642e66c7aSAndreas Gohr            if ($apiCore === null) {
67e4e3d439SAndreas Gohr                $this->coreMethods = (new ApiCore())->getMethods();
6842e66c7aSAndreas Gohr            } else {
696cce3332SAndreas Gohr                $this->coreMethods = $apiCore->getMethods();
7042e66c7aSAndreas Gohr            }
7142e66c7aSAndreas Gohr        }
7242e66c7aSAndreas Gohr        return $this->coreMethods;
7342e66c7aSAndreas Gohr    }
7442e66c7aSAndreas Gohr
7542e66c7aSAndreas Gohr    /**
7642e66c7aSAndreas Gohr     * Collects all the methods of the enabled Remote Plugins
7742e66c7aSAndreas Gohr     *
7842e66c7aSAndreas Gohr     * @return ApiCall[] all plugin methods.
7942e66c7aSAndreas Gohr     */
8042e66c7aSAndreas Gohr    public function getPluginMethods()
8142e66c7aSAndreas Gohr    {
8242e66c7aSAndreas Gohr        if ($this->pluginMethods) return $this->pluginMethods;
8342e66c7aSAndreas Gohr
8442e66c7aSAndreas Gohr        $plugins = plugin_list('remote');
8542e66c7aSAndreas Gohr        foreach ($plugins as $pluginName) {
8642e66c7aSAndreas Gohr            /** @var RemotePlugin $plugin */
8742e66c7aSAndreas Gohr            $plugin = plugin_load('remote', $pluginName);
8842e66c7aSAndreas Gohr            if (!is_subclass_of($plugin, RemotePlugin::class)) {
8942e66c7aSAndreas Gohr                Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin");
9042e66c7aSAndreas Gohr                continue;
9142e66c7aSAndreas Gohr            }
9242e66c7aSAndreas Gohr
9342e66c7aSAndreas Gohr            try {
9442e66c7aSAndreas Gohr                $methods = $plugin->getMethods();
9542e66c7aSAndreas Gohr            } catch (\ReflectionException $e) {
9642e66c7aSAndreas Gohr                Logger::error(
9742e66c7aSAndreas Gohr                    "Remote Plugin $pluginName failed to return methods",
9842e66c7aSAndreas Gohr                    $e->getMessage(),
9942e66c7aSAndreas Gohr                    $e->getFile(),
10042e66c7aSAndreas Gohr                    $e->getLine()
10142e66c7aSAndreas Gohr                );
10242e66c7aSAndreas Gohr                continue;
10342e66c7aSAndreas Gohr            }
10442e66c7aSAndreas Gohr
10542e66c7aSAndreas Gohr            foreach ($methods as $method => $call) {
10642e66c7aSAndreas Gohr                $this->pluginMethods["plugin.$pluginName.$method"] = $call;
10742e66c7aSAndreas Gohr            }
10842e66c7aSAndreas Gohr        }
10942e66c7aSAndreas Gohr
11042e66c7aSAndreas Gohr        return $this->pluginMethods;
11142e66c7aSAndreas Gohr    }
11242e66c7aSAndreas Gohr
11342e66c7aSAndreas Gohr    /**
114dd87735dSAndreas Gohr     * Call a method via remote api.
115dd87735dSAndreas Gohr     *
116dd87735dSAndreas Gohr     * @param string $method name of the method to call.
117dd87735dSAndreas Gohr     * @param array $args arguments to pass to the given method
118dd87735dSAndreas Gohr     * @return mixed result of method call, must be a primitive type.
119dd87735dSAndreas Gohr     * @throws RemoteException
120dd87735dSAndreas Gohr     */
121104a3b7cSAndreas Gohr    public function call($method, $args = [])
122dd87735dSAndreas Gohr    {
123dd87735dSAndreas Gohr        if ($args === null) {
124104a3b7cSAndreas Gohr            $args = [];
125dd87735dSAndreas Gohr        }
126dd87735dSAndreas Gohr
12742e66c7aSAndreas Gohr        // pre-flight checks
12842e66c7aSAndreas Gohr        $this->ensureApiIsEnabled();
12942e66c7aSAndreas Gohr        $methods = $this->getMethods();
13042e66c7aSAndreas Gohr        if (!isset($methods[$method])) {
1311cdd0090SAndreas Gohr            throw new RemoteException('Method does not exist', -32603);
132dd87735dSAndreas Gohr        }
13342e66c7aSAndreas Gohr        $this->ensureAccessIsAllowed($methods[$method]);
134dd87735dSAndreas Gohr
13542e66c7aSAndreas Gohr        // invoke the ApiCall
136e1215f13SPhy        try {
13742e66c7aSAndreas Gohr            return $methods[$method]($args);
138d1f06eb4SAndreas Gohr        } catch (\InvalidArgumentException $e) {
139d1f06eb4SAndreas Gohr            throw new RemoteException($e->getMessage(), -32602);
140d1f06eb4SAndreas Gohr        } catch (\ArgumentCountError $e) {
141d1f06eb4SAndreas Gohr            throw new RemoteException($e->getMessage(), -32602);
142e1215f13SPhy        }
143dd87735dSAndreas Gohr    }
144dd87735dSAndreas Gohr
145dd87735dSAndreas Gohr    /**
14642e66c7aSAndreas Gohr     * Check that the API is generally enabled
147dd87735dSAndreas Gohr     *
148dd87735dSAndreas Gohr     * @return void
14942e66c7aSAndreas Gohr     * @throws RemoteException thrown when the API is disabled
150dd87735dSAndreas Gohr     */
1511468a128SAndreas Gohr    public function ensureApiIsEnabled()
152dd87735dSAndreas Gohr    {
15342e66c7aSAndreas Gohr        global $conf;
15442e66c7aSAndreas Gohr        if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') {
1551468a128SAndreas Gohr            throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604);
15642e66c7aSAndreas Gohr        }
15742e66c7aSAndreas Gohr    }
15842e66c7aSAndreas Gohr
15942e66c7aSAndreas Gohr    /**
16042e66c7aSAndreas Gohr     * Check if the current user is allowed to call the given method
16142e66c7aSAndreas Gohr     *
16242e66c7aSAndreas Gohr     * @param ApiCall $method
16342e66c7aSAndreas Gohr     * @return void
16442e66c7aSAndreas Gohr     * @throws AccessDeniedException Thrown when the user is not allowed to call the method
16542e66c7aSAndreas Gohr     */
1661468a128SAndreas Gohr    public function ensureAccessIsAllowed(ApiCall $method)
16742e66c7aSAndreas Gohr    {
16842e66c7aSAndreas Gohr        global $conf;
16942e66c7aSAndreas Gohr        global $INPUT;
17042e66c7aSAndreas Gohr        global $USERINFO;
17142e66c7aSAndreas Gohr
17242e66c7aSAndreas Gohr        if ($method->isPublic()) return; // public methods are always allowed
17342e66c7aSAndreas Gohr        if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users
17442e66c7aSAndreas Gohr        if (trim($conf['remoteuser']) === '') return; // all users are allowed
17542e66c7aSAndreas Gohr        if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) {
17642e66c7aSAndreas Gohr            return; // user is allowed
17742e66c7aSAndreas Gohr        }
17842e66c7aSAndreas Gohr
17942e66c7aSAndreas Gohr        // still here? no can do
180dd87735dSAndreas Gohr        throw new AccessDeniedException('server error. not authorized to call method', -32604);
181dd87735dSAndreas Gohr    }
182dd87735dSAndreas Gohr}
183