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