xref: /dokuwiki/inc/Remote/JsonRpcServer.php (revision 0c6e917818109b1f50cd46754bb3bf353e36d889)
1<?php
2
3namespace dokuwiki\Remote;
4
5/**
6 * Provide the Remote XMLRPC API as a JSON based API
7 */
8class JsonRpcServer
9{
10    protected $remote;
11
12    /** @var float The XML-RPC Version. 0 is our own simplified variant */
13    protected $version = 0;
14
15    /**
16     * JsonRpcServer constructor.
17     */
18    public function __construct()
19    {
20        $this->remote = new Api();
21        $this->remote->setFileTransformation([$this, 'toFile']);
22    }
23
24    /**
25     * Serve the request
26     *
27     * @return mixed
28     * @throws RemoteException
29     */
30    public function serve()
31    {
32        global $conf;
33        global $INPUT;
34
35        if (!$conf['remote']) {
36            http_status(404);
37            throw new RemoteException("JSON-RPC server not enabled.", -32605);
38        }
39        if (!empty($conf['remotecors'])) {
40            header('Access-Control-Allow-Origin: ' . $conf['remotecors']);
41        }
42        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') {
43            http_status(405);
44            header('Allow: POST');
45            throw new RemoteException("JSON-RPC server only accepts POST requests.", -32606);
46        }
47        if ($INPUT->server->str('CONTENT_TYPE') !== 'application/json') {
48            http_status(415);
49            throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606);
50        }
51
52        try {
53            $body = file_get_contents('php://input');
54            if ($body !== '') {
55                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
56            } else {
57                $data = [];
58            }
59        } catch (\Exception $e) {
60            http_status(400);
61            throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700);
62        }
63
64        return $this->createResponse($data);
65    }
66
67    /**
68     * This executes the method and returns the result
69     *
70     * This should handle all JSON-RPC versions and our simplified version
71     *
72     * @link https://en.wikipedia.org/wiki/JSON-RPC
73     * @link https://www.jsonrpc.org/specification
74     * @param array $data
75     * @return array
76     * @throws RemoteException
77     */
78    protected function createResponse($data)
79    {
80        global $INPUT;
81        $return = [];
82
83        if (isset($data['method'])) {
84            // this is a standard conform request (at least version 1.0)
85            $method = $data['method'];
86            $params = $data['params'] ?? [];
87            $this->version = 1;
88
89            // always return the same ID
90            if (isset($data['id'])) $return['id'] = $data['id'];
91
92            // version 2.0 request
93            if (isset($data['jsonrpc'])) {
94                $return['jsonrpc'] = $data['jsonrpc'];
95                $this->version = (float)$data['jsonrpc'];
96            }
97
98            // version 1.1 request
99            if (isset($data['version'])) {
100                $return['version'] = $data['version'];
101                $this->version = (float)$data['version'];
102            }
103        } else {
104            // this is a simplified request
105            $method = $INPUT->server->str('PATH_INFO');
106            $method = trim($method, '/');
107            $params = $data;
108            $this->version = 0;
109        }
110
111        // excute the method
112        $return['result'] = $this->call($method, $params);
113        $this->addErrorData($return); // handles non-error info
114        return $return;
115    }
116
117    /**
118     * Create an error response
119     *
120     * @param \Exception $exception
121     * @return array
122     */
123    public function returnError($exception)
124    {
125        $return = [];
126        $this->addErrorData($return, $exception);
127        return $return;
128    }
129
130    /**
131     * Depending on the requested version, add error data to the response
132     *
133     * @param array $response
134     * @param \Exception|null $e
135     * @return void
136     */
137    protected function addErrorData(&$response, $e = null)
138    {
139        if ($e !== null) {
140            // error occured, add to response
141            $response['error'] = [
142                'code' => $e->getCode(),
143                'message' => $e->getMessage()
144            ];
145        } else {
146            // no error, act according to version
147            if ($this->version > 0 && $this->version < 2) {
148                // version 1.* wants null
149                $response['error'] = null;
150            } elseif ($this->version < 1) {
151                // simplified version wants success
152                $response['error'] = [
153                    'code' => 0,
154                    'message' => 'success'
155                ];
156            }
157            // version 2 wants no error at all
158        }
159    }
160
161    /**
162     * Call an API method
163     *
164     * @param string $methodname
165     * @param array $args
166     * @return mixed
167     * @throws RemoteException
168     */
169    public function call($methodname, $args)
170    {
171        try {
172            return $this->remote->call($methodname, $args);
173        } catch (AccessDeniedException $e) {
174            if (!isset($_SERVER['REMOTE_USER'])) {
175                http_status(401);
176                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
177            } else {
178                http_status(403);
179                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
180            }
181        } catch (RemoteException $e) {
182            http_status(400);
183            throw $e;
184        }
185    }
186
187    /**
188     * @param string $data
189     * @return string
190     */
191    public function toFile($data)
192    {
193        return base64_encode($data);
194    }
195}
196