xref: /dokuwiki/inc/Remote/JsonRpcServer.php (revision 9866f25121a88ca088781fb3bc173305af199fad)
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    }
22
23    /**
24     * Serve the request
25     *
26     * @param string $body Should only be set for testing, otherwise the request body is read from php://input
27     * @return mixed
28     * @throws RemoteException
29     */
30    public function serve($body = '')
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            if($body === '') {
54                $body = file_get_contents('php://input');
55            }
56            if($body !== '') {
57                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
58            } else {
59                $data = [];
60            }
61        } catch (\Exception $e) {
62            http_status(400);
63            throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700);
64        }
65
66        return $this->createResponse($data);
67    }
68
69    /**
70     * This executes the method and returns the result
71     *
72     * This should handle all JSON-RPC versions and our simplified version
73     *
74     * @link https://en.wikipedia.org/wiki/JSON-RPC
75     * @link https://www.jsonrpc.org/specification
76     * @param array $data
77     * @return array
78     * @throws RemoteException
79     */
80    protected function createResponse($data)
81    {
82        global $INPUT;
83        $return = [];
84
85        if (isset($data['method'])) {
86            // this is a standard conform request (at least version 1.0)
87            $method = $data['method'];
88            $params = $data['params'] ?? [];
89            $this->version = 1;
90
91            // always return the same ID
92            if (isset($data['id'])) $return['id'] = $data['id'];
93
94            // version 2.0 request
95            if (isset($data['jsonrpc'])) {
96                $return['jsonrpc'] = $data['jsonrpc'];
97                $this->version = (float)$data['jsonrpc'];
98            }
99
100            // version 1.1 request
101            if (isset($data['version'])) {
102                $return['version'] = $data['version'];
103                $this->version = (float)$data['version'];
104            }
105        } else {
106            // this is a simplified request
107            $method = $INPUT->server->str('PATH_INFO');
108            $method = trim($method, '/');
109            $params = $data;
110            $this->version = 0;
111        }
112
113        // excute the method
114        $return['result'] = $this->call($method, $params);
115        $this->addErrorData($return); // handles non-error info
116        return $return;
117    }
118
119    /**
120     * Create an error response
121     *
122     * @param \Exception $exception
123     * @return array
124     */
125    public function returnError($exception)
126    {
127        $return = [];
128        $this->addErrorData($return, $exception);
129        return $return;
130    }
131
132    /**
133     * Depending on the requested version, add error data to the response
134     *
135     * @param array $response
136     * @param \Exception|null $e
137     * @return void
138     */
139    protected function addErrorData(&$response, $e = null)
140    {
141        if ($e !== null) {
142            // error occured, add to response
143            $response['error'] = [
144                'code' => $e->getCode(),
145                'message' => $e->getMessage()
146            ];
147        } else {
148            // no error, act according to version
149            if ($this->version > 0 && $this->version < 2) {
150                // version 1.* wants null
151                $response['error'] = null;
152            } elseif ($this->version < 1) {
153                // simplified version wants success
154                $response['error'] = [
155                    'code' => 0,
156                    'message' => 'success'
157                ];
158            }
159            // version 2 wants no error at all
160        }
161    }
162
163    /**
164     * Call an API method
165     *
166     * @param string $methodname
167     * @param array $args
168     * @return mixed
169     * @throws RemoteException
170     */
171    public function call($methodname, $args)
172    {
173        try {
174            return $this->remote->call($methodname, $args);
175        } catch (AccessDeniedException $e) {
176            if (!isset($_SERVER['REMOTE_USER'])) {
177                http_status(401);
178                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
179            } else {
180                http_status(403);
181                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
182            }
183        } catch (RemoteException $e) {
184            http_status(400);
185            throw $e;
186        }
187    }
188}
189