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