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 } 47ba15f985SAndreas Gohr [$contentType] = explode(';', $INPUT->server->str('CONTENT_TYPE'), 2); // ignore charset 48ba15f985SAndreas Gohr $contentType = strtolower($contentType); // mime types are case-insensitive 49ba15f985SAndreas 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'] = [ 146*5c3fa123SAndreas Gohr 'code' => $e->getCode() ?: 1, // 0 is success, so we use 1 as default 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