xref: /dokuwiki/inc/Remote/JsonRpcServer.php (revision ba15f985c4b85f33c267c8f76eefdc0d8bd97e11)
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();
21f657e5d0SAndreas Gohr    }
22f657e5d0SAndreas Gohr
23f657e5d0SAndreas Gohr    /**
24f657e5d0SAndreas Gohr     * Serve the request
25f657e5d0SAndreas Gohr     *
265e47e6dfSAndreas Gohr     * @param string $body Should only be set for testing, otherwise the request body is read from php://input
27f657e5d0SAndreas Gohr     * @return mixed
28f657e5d0SAndreas Gohr     * @throws RemoteException
29f657e5d0SAndreas Gohr     */
305e47e6dfSAndreas Gohr    public function serve($body = '')
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        }
47*ba15f985SAndreas Gohr        [$contentType] = explode(';', $INPUT->server->str('CONTENT_TYPE'), 2); // ignore charset
48*ba15f985SAndreas Gohr        $contentType = strtolower($contentType); // mime types are case-insensitive
49*ba15f985SAndreas Gohr        if ($contentType !== 'application/json') {
508fae2e99SAndreas Gohr            http_status(415);
518fae2e99SAndreas Gohr            throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606);
528fae2e99SAndreas Gohr        }
53f657e5d0SAndreas Gohr
5487603a0aSAndreas Gohr        try {
555e47e6dfSAndreas Gohr            if ($body === '') {
568d294593SAndreas Gohr                $body = file_get_contents('php://input');
575e47e6dfSAndreas Gohr            }
588d294593SAndreas Gohr            if ($body !== '') {
598d294593SAndreas Gohr                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
608d294593SAndreas Gohr            } else {
616f8e03f5SAndreas Gohr                $data = [];
6287603a0aSAndreas Gohr            }
638d294593SAndreas Gohr        } catch (\Exception $e) {
648d294593SAndreas Gohr            http_status(400);
658d294593SAndreas Gohr            throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700);
668d294593SAndreas Gohr        }
67f657e5d0SAndreas Gohr
686f8e03f5SAndreas Gohr        return $this->createResponse($data);
696f8e03f5SAndreas Gohr    }
706f8e03f5SAndreas Gohr
716f8e03f5SAndreas Gohr    /**
726f8e03f5SAndreas Gohr     * This executes the method and returns the result
736f8e03f5SAndreas Gohr     *
746f8e03f5SAndreas Gohr     * This should handle all JSON-RPC versions and our simplified version
756f8e03f5SAndreas Gohr     *
766f8e03f5SAndreas Gohr     * @link https://en.wikipedia.org/wiki/JSON-RPC
776f8e03f5SAndreas Gohr     * @link https://www.jsonrpc.org/specification
786f8e03f5SAndreas Gohr     * @param array $data
796f8e03f5SAndreas Gohr     * @return array
806f8e03f5SAndreas Gohr     * @throws RemoteException
816f8e03f5SAndreas Gohr     */
826f8e03f5SAndreas Gohr    protected function createResponse($data)
836f8e03f5SAndreas Gohr    {
846f8e03f5SAndreas Gohr        global $INPUT;
856f8e03f5SAndreas Gohr        $return = [];
866f8e03f5SAndreas Gohr
876f8e03f5SAndreas Gohr        if (isset($data['method'])) {
886f8e03f5SAndreas Gohr            // this is a standard conform request (at least version 1.0)
896f8e03f5SAndreas Gohr            $method = $data['method'];
906f8e03f5SAndreas Gohr            $params = $data['params'] ?? [];
916f8e03f5SAndreas Gohr            $this->version = 1;
926f8e03f5SAndreas Gohr
936f8e03f5SAndreas Gohr            // always return the same ID
946f8e03f5SAndreas Gohr            if (isset($data['id'])) $return['id'] = $data['id'];
956f8e03f5SAndreas Gohr
966f8e03f5SAndreas Gohr            // version 2.0 request
976f8e03f5SAndreas Gohr            if (isset($data['jsonrpc'])) {
986f8e03f5SAndreas Gohr                $return['jsonrpc'] = $data['jsonrpc'];
996f8e03f5SAndreas Gohr                $this->version = (float)$data['jsonrpc'];
1006f8e03f5SAndreas Gohr            }
1016f8e03f5SAndreas Gohr
1026f8e03f5SAndreas Gohr            // version 1.1 request
1036f8e03f5SAndreas Gohr            if (isset($data['version'])) {
1046f8e03f5SAndreas Gohr                $return['version'] = $data['version'];
1056f8e03f5SAndreas Gohr                $this->version = (float)$data['version'];
1066f8e03f5SAndreas Gohr            }
1076f8e03f5SAndreas Gohr        } else {
1086f8e03f5SAndreas Gohr            // this is a simplified request
1096f8e03f5SAndreas Gohr            $method = $INPUT->server->str('PATH_INFO');
1106f8e03f5SAndreas Gohr            $method = trim($method, '/');
1116f8e03f5SAndreas Gohr            $params = $data;
1126f8e03f5SAndreas Gohr            $this->version = 0;
1136f8e03f5SAndreas Gohr        }
1146f8e03f5SAndreas Gohr
1156f8e03f5SAndreas Gohr        // excute the method
1166f8e03f5SAndreas Gohr        $return['result'] = $this->call($method, $params);
1176f8e03f5SAndreas Gohr        $this->addErrorData($return); // handles non-error info
1186f8e03f5SAndreas Gohr        return $return;
1196f8e03f5SAndreas Gohr    }
1206f8e03f5SAndreas Gohr
1216f8e03f5SAndreas Gohr    /**
1226f8e03f5SAndreas Gohr     * Create an error response
1236f8e03f5SAndreas Gohr     *
1246f8e03f5SAndreas Gohr     * @param \Exception $exception
1256f8e03f5SAndreas Gohr     * @return array
1266f8e03f5SAndreas Gohr     */
1276f8e03f5SAndreas Gohr    public function returnError($exception)
1286f8e03f5SAndreas Gohr    {
1296f8e03f5SAndreas Gohr        $return = [];
1306f8e03f5SAndreas Gohr        $this->addErrorData($return, $exception);
1316f8e03f5SAndreas Gohr        return $return;
1326f8e03f5SAndreas Gohr    }
1336f8e03f5SAndreas Gohr
1346f8e03f5SAndreas Gohr    /**
1356f8e03f5SAndreas Gohr     * Depending on the requested version, add error data to the response
1366f8e03f5SAndreas Gohr     *
1376f8e03f5SAndreas Gohr     * @param array $response
1386f8e03f5SAndreas Gohr     * @param \Exception|null $e
1396f8e03f5SAndreas Gohr     * @return void
1406f8e03f5SAndreas Gohr     */
1416f8e03f5SAndreas Gohr    protected function addErrorData(&$response, $e = null)
1426f8e03f5SAndreas Gohr    {
1436f8e03f5SAndreas Gohr        if ($e !== null) {
1446f8e03f5SAndreas Gohr            // error occured, add to response
1456f8e03f5SAndreas Gohr            $response['error'] = [
1466f8e03f5SAndreas Gohr                'code' => $e->getCode(),
1476f8e03f5SAndreas Gohr                'message' => $e->getMessage()
1486f8e03f5SAndreas Gohr            ];
1496f8e03f5SAndreas Gohr        } else {
1506f8e03f5SAndreas Gohr            // no error, act according to version
1516f8e03f5SAndreas Gohr            if ($this->version > 0 && $this->version < 2) {
1526f8e03f5SAndreas Gohr                // version 1.* wants null
1536f8e03f5SAndreas Gohr                $response['error'] = null;
1546f8e03f5SAndreas Gohr            } elseif ($this->version < 1) {
1556f8e03f5SAndreas Gohr                // simplified version wants success
1566f8e03f5SAndreas Gohr                $response['error'] = [
1576f8e03f5SAndreas Gohr                    'code' => 0,
1586f8e03f5SAndreas Gohr                    'message' => 'success'
1596f8e03f5SAndreas Gohr                ];
1606f8e03f5SAndreas Gohr            }
1616f8e03f5SAndreas Gohr            // version 2 wants no error at all
1626f8e03f5SAndreas Gohr        }
163f657e5d0SAndreas Gohr    }
164f657e5d0SAndreas Gohr
165f657e5d0SAndreas Gohr    /**
166f657e5d0SAndreas Gohr     * Call an API method
167f657e5d0SAndreas Gohr     *
168f657e5d0SAndreas Gohr     * @param string $methodname
169f657e5d0SAndreas Gohr     * @param array $args
170f657e5d0SAndreas Gohr     * @return mixed
171f657e5d0SAndreas Gohr     * @throws RemoteException
172f657e5d0SAndreas Gohr     */
173f657e5d0SAndreas Gohr    public function call($methodname, $args)
174f657e5d0SAndreas Gohr    {
175f657e5d0SAndreas Gohr        try {
17642e66c7aSAndreas Gohr            return $this->remote->call($methodname, $args);
177f657e5d0SAndreas Gohr        } catch (AccessDeniedException $e) {
178f657e5d0SAndreas Gohr            if (!isset($_SERVER['REMOTE_USER'])) {
179f657e5d0SAndreas Gohr                http_status(401);
180f657e5d0SAndreas Gohr                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
181f657e5d0SAndreas Gohr            } else {
182f657e5d0SAndreas Gohr                http_status(403);
183f657e5d0SAndreas Gohr                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
184f657e5d0SAndreas Gohr            }
185f657e5d0SAndreas Gohr        } catch (RemoteException $e) {
186f657e5d0SAndreas Gohr            http_status(400);
187f657e5d0SAndreas Gohr            throw $e;
188f657e5d0SAndreas Gohr        }
189f657e5d0SAndreas Gohr    }
190f657e5d0SAndreas Gohr}
191