xref: /dokuwiki/inc/Remote/JsonRpcServer.php (revision 6f8e03f5bc790d26e1215349cea98d5a73654139)
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
13*6f8e03f5SAndreas Gohr    /** @var float The XML-RPC Version. 0 is our own simplified variant */
14*6f8e03f5SAndreas Gohr    protected $version = 0;
15*6f8e03f5SAndreas 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*6f8e03f5SAndreas Gohr            $data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
5587603a0aSAndreas Gohr        } catch (\Exception $e) {
56*6f8e03f5SAndreas Gohr            $data = [];
5787603a0aSAndreas Gohr        }
58f657e5d0SAndreas Gohr
59*6f8e03f5SAndreas Gohr        return $this->createResponse($data);
60*6f8e03f5SAndreas Gohr    }
61*6f8e03f5SAndreas Gohr
62*6f8e03f5SAndreas Gohr    /**
63*6f8e03f5SAndreas Gohr     * This executes the method and returns the result
64*6f8e03f5SAndreas Gohr     *
65*6f8e03f5SAndreas Gohr     * This should handle all JSON-RPC versions and our simplified version
66*6f8e03f5SAndreas Gohr     *
67*6f8e03f5SAndreas Gohr     * @link https://en.wikipedia.org/wiki/JSON-RPC
68*6f8e03f5SAndreas Gohr     * @link https://www.jsonrpc.org/specification
69*6f8e03f5SAndreas Gohr     * @param array $data
70*6f8e03f5SAndreas Gohr     * @return array
71*6f8e03f5SAndreas Gohr     * @throws RemoteException
72*6f8e03f5SAndreas Gohr     */
73*6f8e03f5SAndreas Gohr    protected function createResponse($data)
74*6f8e03f5SAndreas Gohr    {
75*6f8e03f5SAndreas Gohr        global $INPUT;
76*6f8e03f5SAndreas Gohr        $return = [];
77*6f8e03f5SAndreas Gohr
78*6f8e03f5SAndreas Gohr        if (isset($data['method'])) {
79*6f8e03f5SAndreas Gohr            // this is a standard conform request (at least version 1.0)
80*6f8e03f5SAndreas Gohr            $method = $data['method'];
81*6f8e03f5SAndreas Gohr            $params = $data['params'] ?? [];
82*6f8e03f5SAndreas Gohr            $this->version = 1;
83*6f8e03f5SAndreas Gohr
84*6f8e03f5SAndreas Gohr            // always return the same ID
85*6f8e03f5SAndreas Gohr            if (isset($data['id'])) $return['id'] = $data['id'];
86*6f8e03f5SAndreas Gohr
87*6f8e03f5SAndreas Gohr            // version 2.0 request
88*6f8e03f5SAndreas Gohr            if (isset($data['jsonrpc'])) {
89*6f8e03f5SAndreas Gohr                $return['jsonrpc'] = $data['jsonrpc'];
90*6f8e03f5SAndreas Gohr                $this->version = (float)$data['jsonrpc'];
91*6f8e03f5SAndreas Gohr            }
92*6f8e03f5SAndreas Gohr
93*6f8e03f5SAndreas Gohr            // version 1.1 request
94*6f8e03f5SAndreas Gohr            if (isset($data['version'])) {
95*6f8e03f5SAndreas Gohr                $return['version'] = $data['version'];
96*6f8e03f5SAndreas Gohr                $this->version = (float)$data['version'];
97*6f8e03f5SAndreas Gohr            }
98*6f8e03f5SAndreas Gohr        } else {
99*6f8e03f5SAndreas Gohr            // this is a simplified request
100*6f8e03f5SAndreas Gohr            $method = $INPUT->server->str('PATH_INFO');
101*6f8e03f5SAndreas Gohr            $method = trim($method, '/');
102*6f8e03f5SAndreas Gohr            $params = $data;
103*6f8e03f5SAndreas Gohr            $this->version = 0;
104*6f8e03f5SAndreas Gohr        }
105*6f8e03f5SAndreas Gohr
106*6f8e03f5SAndreas Gohr        // excute the method
107*6f8e03f5SAndreas Gohr        $return['result'] = $this->call($method, $params);
108*6f8e03f5SAndreas Gohr        $this->addErrorData($return); // handles non-error info
109*6f8e03f5SAndreas Gohr        return $return;
110*6f8e03f5SAndreas Gohr    }
111*6f8e03f5SAndreas Gohr
112*6f8e03f5SAndreas Gohr    /**
113*6f8e03f5SAndreas Gohr     * Create an error response
114*6f8e03f5SAndreas Gohr     *
115*6f8e03f5SAndreas Gohr     * @param \Exception $exception
116*6f8e03f5SAndreas Gohr     * @return array
117*6f8e03f5SAndreas Gohr     */
118*6f8e03f5SAndreas Gohr    public function returnError($exception)
119*6f8e03f5SAndreas Gohr    {
120*6f8e03f5SAndreas Gohr        $return = [];
121*6f8e03f5SAndreas Gohr        $this->addErrorData($return, $exception);
122*6f8e03f5SAndreas Gohr        return $return;
123*6f8e03f5SAndreas Gohr    }
124*6f8e03f5SAndreas Gohr
125*6f8e03f5SAndreas Gohr    /**
126*6f8e03f5SAndreas Gohr     * Depending on the requested version, add error data to the response
127*6f8e03f5SAndreas Gohr     *
128*6f8e03f5SAndreas Gohr     * @param array $response
129*6f8e03f5SAndreas Gohr     * @param \Exception|null $e
130*6f8e03f5SAndreas Gohr     * @return void
131*6f8e03f5SAndreas Gohr     */
132*6f8e03f5SAndreas Gohr    protected function addErrorData(&$response, $e = null)
133*6f8e03f5SAndreas Gohr    {
134*6f8e03f5SAndreas Gohr        if ($e !== null) {
135*6f8e03f5SAndreas Gohr            // error occured, add to response
136*6f8e03f5SAndreas Gohr            $response['error'] = [
137*6f8e03f5SAndreas Gohr                'code' => $e->getCode(),
138*6f8e03f5SAndreas Gohr                'message' => $e->getMessage()
139*6f8e03f5SAndreas Gohr            ];
140*6f8e03f5SAndreas Gohr        } else {
141*6f8e03f5SAndreas Gohr            // no error, act according to version
142*6f8e03f5SAndreas Gohr            if ($this->version > 0 && $this->version < 2) {
143*6f8e03f5SAndreas Gohr                // version 1.* wants null
144*6f8e03f5SAndreas Gohr                $response['error'] = null;
145*6f8e03f5SAndreas Gohr            } elseif ($this->version < 1) {
146*6f8e03f5SAndreas Gohr                // simplified version wants success
147*6f8e03f5SAndreas Gohr                $response['error'] = [
148*6f8e03f5SAndreas Gohr                    'code' => 0,
149*6f8e03f5SAndreas Gohr                    'message' => 'success'
150*6f8e03f5SAndreas Gohr                ];
151*6f8e03f5SAndreas Gohr            }
152*6f8e03f5SAndreas Gohr            // version 2 wants no error at all
153*6f8e03f5SAndreas Gohr        }
154f657e5d0SAndreas Gohr    }
155f657e5d0SAndreas Gohr
156f657e5d0SAndreas Gohr    /**
157f657e5d0SAndreas Gohr     * Call an API method
158f657e5d0SAndreas Gohr     *
159f657e5d0SAndreas Gohr     * @param string $methodname
160f657e5d0SAndreas Gohr     * @param array $args
161f657e5d0SAndreas Gohr     * @return mixed
162f657e5d0SAndreas Gohr     * @throws RemoteException
163f657e5d0SAndreas Gohr     */
164f657e5d0SAndreas Gohr    public function call($methodname, $args)
165f657e5d0SAndreas Gohr    {
166f657e5d0SAndreas Gohr        try {
167f657e5d0SAndreas Gohr            $result = $this->remote->call($methodname, $args);
168f657e5d0SAndreas Gohr            return $result;
169f657e5d0SAndreas Gohr        } catch (AccessDeniedException $e) {
170f657e5d0SAndreas Gohr            if (!isset($_SERVER['REMOTE_USER'])) {
171f657e5d0SAndreas Gohr                http_status(401);
172f657e5d0SAndreas Gohr                throw new RemoteException("server error. not authorized to call method $methodname", -32603);
173f657e5d0SAndreas Gohr            } else {
174f657e5d0SAndreas Gohr                http_status(403);
175f657e5d0SAndreas Gohr                throw new RemoteException("server error. forbidden to call the method $methodname", -32604);
176f657e5d0SAndreas Gohr            }
177f657e5d0SAndreas Gohr        } catch (RemoteException $e) {
178f657e5d0SAndreas Gohr            http_status(400);
179f657e5d0SAndreas Gohr            throw $e;
180f657e5d0SAndreas Gohr        }
181f657e5d0SAndreas Gohr    }
182f657e5d0SAndreas Gohr
183f657e5d0SAndreas Gohr    /**
184f657e5d0SAndreas Gohr     * @param string $data
185f657e5d0SAndreas Gohr     * @return string
186f657e5d0SAndreas Gohr     */
187f657e5d0SAndreas Gohr    public function toFile($data)
188f657e5d0SAndreas Gohr    {
189f657e5d0SAndreas Gohr        return base64_encode($data);
190f657e5d0SAndreas Gohr    }
191f657e5d0SAndreas Gohr
192f657e5d0SAndreas Gohr}
193