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