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