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