xref: /dokuwiki/inc/Remote/JsonRpcServer.php (revision 42e66c7a3a46f444860e00bbf95403ac2b5cbc8f)
1f657e5d0SAndreas Gohr<?php
2f657e5d0SAndreas Gohr
3f657e5d0SAndreas Gohrnamespace dokuwiki\Remote;
4f657e5d0SAndreas Gohr
5f657e5d0SAndreas Gohr/**
6f657e5d0SAndreas Gohr * Provide the Remote XMLRPC API as a JSON based API
7f657e5d0SAndreas Gohr */
8f657e5d0SAndreas Gohrclass JsonRpcServer
9f657e5d0SAndreas Gohr{
10f657e5d0SAndreas Gohr    protected $remote;
11f657e5d0SAndreas Gohr
126f8e03f5SAndreas Gohr    /** @var float The XML-RPC Version. 0 is our own simplified variant */
136f8e03f5SAndreas Gohr    protected $version = 0;
146f8e03f5SAndreas Gohr
15f657e5d0SAndreas Gohr    /**
16f657e5d0SAndreas Gohr     * JsonRpcServer constructor.
17f657e5d0SAndreas Gohr     */
18f657e5d0SAndreas Gohr    public function __construct()
19f657e5d0SAndreas Gohr    {
20f657e5d0SAndreas Gohr        $this->remote = new Api();
21cf927d07Ssplitbrain        $this->remote->setFileTransformation([$this, 'toFile']);
22f657e5d0SAndreas Gohr    }
23f657e5d0SAndreas Gohr
24f657e5d0SAndreas Gohr    /**
25f657e5d0SAndreas Gohr     * Serve the request
26f657e5d0SAndreas Gohr     *
27f657e5d0SAndreas Gohr     * @return mixed
28f657e5d0SAndreas Gohr     * @throws RemoteException
29f657e5d0SAndreas Gohr     */
30f657e5d0SAndreas Gohr    public function serve()
31f657e5d0SAndreas Gohr    {
32f657e5d0SAndreas Gohr        global $conf;
338fae2e99SAndreas Gohr        global $INPUT;
348fae2e99SAndreas Gohr
35f657e5d0SAndreas Gohr        if (!$conf['remote']) {
36f657e5d0SAndreas Gohr            http_status(404);
37f657e5d0SAndreas Gohr            throw new RemoteException("JSON-RPC server not enabled.", -32605);
38f657e5d0SAndreas Gohr        }
39f657e5d0SAndreas Gohr        if (!empty($conf['remotecors'])) {
40f657e5d0SAndreas Gohr            header('Access-Control-Allow-Origin: ' . $conf['remotecors']);
41f657e5d0SAndreas Gohr        }
428fae2e99SAndreas Gohr        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') {
438fae2e99SAndreas Gohr            http_status(405);
448fae2e99SAndreas Gohr            header('Allow: POST');
458fae2e99SAndreas Gohr            throw new RemoteException("JSON-RPC server only accepts POST requests.", -32606);
468fae2e99SAndreas Gohr        }
478fae2e99SAndreas Gohr        if ($INPUT->server->str('CONTENT_TYPE') !== 'application/json') {
488fae2e99SAndreas Gohr            http_status(415);
498fae2e99SAndreas Gohr            throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606);
508fae2e99SAndreas Gohr        }
51f657e5d0SAndreas Gohr
5287603a0aSAndreas Gohr        try {
538d294593SAndreas Gohr            $body = file_get_contents('php://input');
548d294593SAndreas Gohr            if ($body !== '') {
558d294593SAndreas Gohr                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
568d294593SAndreas Gohr            } else {
576f8e03f5SAndreas Gohr                $data = [];
5887603a0aSAndreas Gohr            }
598d294593SAndreas Gohr        } catch (\Exception $e) {
608d294593SAndreas Gohr            http_status(400);
618d294593SAndreas Gohr            throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700);
628d294593SAndreas Gohr        }
63f657e5d0SAndreas Gohr
646f8e03f5SAndreas Gohr        return $this->createResponse($data);
656f8e03f5SAndreas Gohr    }
666f8e03f5SAndreas Gohr
676f8e03f5SAndreas Gohr    /**
686f8e03f5SAndreas Gohr     * This executes the method and returns the result
696f8e03f5SAndreas Gohr     *
706f8e03f5SAndreas Gohr     * This should handle all JSON-RPC versions and our simplified version
716f8e03f5SAndreas Gohr     *
726f8e03f5SAndreas Gohr     * @link https://en.wikipedia.org/wiki/JSON-RPC
736f8e03f5SAndreas Gohr     * @link https://www.jsonrpc.org/specification
746f8e03f5SAndreas Gohr     * @param array $data
756f8e03f5SAndreas Gohr     * @return array
766f8e03f5SAndreas Gohr     * @throws RemoteException
776f8e03f5SAndreas Gohr     */
786f8e03f5SAndreas Gohr    protected function createResponse($data)
796f8e03f5SAndreas Gohr    {
806f8e03f5SAndreas Gohr        global $INPUT;
816f8e03f5SAndreas Gohr        $return = [];
826f8e03f5SAndreas Gohr
836f8e03f5SAndreas Gohr        if (isset($data['method'])) {
846f8e03f5SAndreas Gohr            // this is a standard conform request (at least version 1.0)
856f8e03f5SAndreas Gohr            $method = $data['method'];
866f8e03f5SAndreas Gohr            $params = $data['params'] ?? [];
876f8e03f5SAndreas Gohr            $this->version = 1;
886f8e03f5SAndreas Gohr
896f8e03f5SAndreas Gohr            // always return the same ID
906f8e03f5SAndreas Gohr            if (isset($data['id'])) $return['id'] = $data['id'];
916f8e03f5SAndreas Gohr
926f8e03f5SAndreas Gohr            // version 2.0 request
936f8e03f5SAndreas Gohr            if (isset($data['jsonrpc'])) {
946f8e03f5SAndreas Gohr                $return['jsonrpc'] = $data['jsonrpc'];
956f8e03f5SAndreas Gohr                $this->version = (float)$data['jsonrpc'];
966f8e03f5SAndreas Gohr            }
976f8e03f5SAndreas Gohr
986f8e03f5SAndreas Gohr            // version 1.1 request
996f8e03f5SAndreas Gohr            if (isset($data['version'])) {
1006f8e03f5SAndreas Gohr                $return['version'] = $data['version'];
1016f8e03f5SAndreas Gohr                $this->version = (float)$data['version'];
1026f8e03f5SAndreas Gohr            }
1036f8e03f5SAndreas Gohr        } else {
1046f8e03f5SAndreas Gohr            // this is a simplified request
1056f8e03f5SAndreas Gohr            $method = $INPUT->server->str('PATH_INFO');
1066f8e03f5SAndreas Gohr            $method = trim($method, '/');
1076f8e03f5SAndreas Gohr            $params = $data;
1086f8e03f5SAndreas Gohr            $this->version = 0;
1096f8e03f5SAndreas Gohr        }
1106f8e03f5SAndreas Gohr
1116f8e03f5SAndreas Gohr        // excute the method
1126f8e03f5SAndreas Gohr        $return['result'] = $this->call($method, $params);
1136f8e03f5SAndreas Gohr        $this->addErrorData($return); // handles non-error info
1146f8e03f5SAndreas Gohr        return $return;
1156f8e03f5SAndreas Gohr    }
1166f8e03f5SAndreas Gohr
1176f8e03f5SAndreas Gohr    /**
1186f8e03f5SAndreas Gohr     * Create an error response
1196f8e03f5SAndreas Gohr     *
1206f8e03f5SAndreas Gohr     * @param \Exception $exception
1216f8e03f5SAndreas Gohr     * @return array
1226f8e03f5SAndreas Gohr     */
1236f8e03f5SAndreas Gohr    public function returnError($exception)
1246f8e03f5SAndreas Gohr    {
1256f8e03f5SAndreas Gohr        $return = [];
1266f8e03f5SAndreas Gohr        $this->addErrorData($return, $exception);
1276f8e03f5SAndreas Gohr        return $return;
1286f8e03f5SAndreas Gohr    }
1296f8e03f5SAndreas Gohr
1306f8e03f5SAndreas Gohr    /**
1316f8e03f5SAndreas Gohr     * Depending on the requested version, add error data to the response
1326f8e03f5SAndreas Gohr     *
1336f8e03f5SAndreas Gohr     * @param array $response
1346f8e03f5SAndreas Gohr     * @param \Exception|null $e
1356f8e03f5SAndreas Gohr     * @return void
1366f8e03f5SAndreas Gohr     */
1376f8e03f5SAndreas Gohr    protected function addErrorData(&$response, $e = null)
1386f8e03f5SAndreas Gohr    {
1396f8e03f5SAndreas Gohr        if ($e !== null) {
1406f8e03f5SAndreas Gohr            // error occured, add to response
1416f8e03f5SAndreas Gohr            $response['error'] = [
1426f8e03f5SAndreas Gohr                'code' => $e->getCode(),
1436f8e03f5SAndreas Gohr                'message' => $e->getMessage()
1446f8e03f5SAndreas Gohr            ];
1456f8e03f5SAndreas Gohr        } else {
1466f8e03f5SAndreas Gohr            // no error, act according to version
1476f8e03f5SAndreas Gohr            if ($this->version > 0 && $this->version < 2) {
1486f8e03f5SAndreas Gohr                // version 1.* wants null
1496f8e03f5SAndreas Gohr                $response['error'] = null;
1506f8e03f5SAndreas Gohr            } elseif ($this->version < 1) {
1516f8e03f5SAndreas Gohr                // simplified version wants success
1526f8e03f5SAndreas Gohr                $response['error'] = [
1536f8e03f5SAndreas Gohr                    'code' => 0,
1546f8e03f5SAndreas Gohr                    'message' => 'success'
1556f8e03f5SAndreas Gohr                ];
1566f8e03f5SAndreas Gohr            }
1576f8e03f5SAndreas Gohr            // version 2 wants no error at all
1586f8e03f5SAndreas Gohr        }
159f657e5d0SAndreas Gohr    }
160f657e5d0SAndreas Gohr
161f657e5d0SAndreas Gohr    /**
162f657e5d0SAndreas Gohr     * Call an API method
163f657e5d0SAndreas Gohr     *
164f657e5d0SAndreas Gohr     * @param string $methodname
165f657e5d0SAndreas Gohr     * @param array $args
166f657e5d0SAndreas Gohr     * @return mixed
167f657e5d0SAndreas Gohr     * @throws RemoteException
168f657e5d0SAndreas Gohr     */
169f657e5d0SAndreas Gohr    public function call($methodname, $args)
170f657e5d0SAndreas Gohr    {
171f657e5d0SAndreas Gohr        try {
172*42e66c7aSAndreas Gohr            return $this->remote->call($methodname, $args);
173f657e5d0SAndreas Gohr        } catch (AccessDeniedException $e) {
174f657e5d0SAndreas Gohr            if (!isset($_SERVER['REMOTE_USER'])) {
175f657e5d0SAndreas Gohr                http_status(401);
176f657e5d0SAndreas Gohr                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
177f657e5d0SAndreas Gohr            } else {
178f657e5d0SAndreas Gohr                http_status(403);
179f657e5d0SAndreas Gohr                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
180f657e5d0SAndreas Gohr            }
181f657e5d0SAndreas Gohr        } catch (RemoteException $e) {
182f657e5d0SAndreas Gohr            http_status(400);
183f657e5d0SAndreas Gohr            throw $e;
184f657e5d0SAndreas Gohr        }
185f657e5d0SAndreas Gohr    }
186f657e5d0SAndreas Gohr
187f657e5d0SAndreas Gohr    /**
188f657e5d0SAndreas Gohr     * @param string $data
189f657e5d0SAndreas Gohr     * @return string
190f657e5d0SAndreas Gohr     */
191f657e5d0SAndreas Gohr    public function toFile($data)
192f657e5d0SAndreas Gohr    {
193f657e5d0SAndreas Gohr        return base64_encode($data);
194f657e5d0SAndreas Gohr    }
195f657e5d0SAndreas Gohr}
196