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(); 21cf927d07Ssplitbrain $this->remote->setFileTransformation([$this, 'toFile']); 22f657e5d0SAndreas Gohr } 23f657e5d0SAndreas Gohr 24f657e5d0SAndreas Gohr /** 25f657e5d0SAndreas Gohr * Serve the request 26f657e5d0SAndreas Gohr * 27f657e5d0SAndreas Gohr * @return mixed 28f657e5d0SAndreas Gohr * @throws RemoteException 29f657e5d0SAndreas Gohr */ 30f657e5d0SAndreas Gohr public function serve() 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 } 478fae2e99SAndreas Gohr if ($INPUT->server->str('CONTENT_TYPE') !== 'application/json') { 488fae2e99SAndreas Gohr http_status(415); 498fae2e99SAndreas Gohr throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606); 508fae2e99SAndreas Gohr } 51f657e5d0SAndreas Gohr 5287603a0aSAndreas Gohr try { 538d294593SAndreas Gohr $body = file_get_contents('php://input'); 548d294593SAndreas Gohr if ($body !== '') { 558d294593SAndreas Gohr $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); 568d294593SAndreas Gohr } else { 576f8e03f5SAndreas Gohr $data = []; 5887603a0aSAndreas Gohr } 598d294593SAndreas Gohr } catch (\Exception $e) { 608d294593SAndreas Gohr http_status(400); 618d294593SAndreas Gohr throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700); 628d294593SAndreas Gohr } 63f657e5d0SAndreas Gohr 646f8e03f5SAndreas Gohr return $this->createResponse($data); 656f8e03f5SAndreas Gohr } 666f8e03f5SAndreas Gohr 676f8e03f5SAndreas Gohr /** 686f8e03f5SAndreas Gohr * This executes the method and returns the result 696f8e03f5SAndreas Gohr * 706f8e03f5SAndreas Gohr * This should handle all JSON-RPC versions and our simplified version 716f8e03f5SAndreas Gohr * 726f8e03f5SAndreas Gohr * @link https://en.wikipedia.org/wiki/JSON-RPC 736f8e03f5SAndreas Gohr * @link https://www.jsonrpc.org/specification 746f8e03f5SAndreas Gohr * @param array $data 756f8e03f5SAndreas Gohr * @return array 766f8e03f5SAndreas Gohr * @throws RemoteException 776f8e03f5SAndreas Gohr */ 786f8e03f5SAndreas Gohr protected function createResponse($data) 796f8e03f5SAndreas Gohr { 806f8e03f5SAndreas Gohr global $INPUT; 816f8e03f5SAndreas Gohr $return = []; 826f8e03f5SAndreas Gohr 836f8e03f5SAndreas Gohr if (isset($data['method'])) { 846f8e03f5SAndreas Gohr // this is a standard conform request (at least version 1.0) 856f8e03f5SAndreas Gohr $method = $data['method']; 866f8e03f5SAndreas Gohr $params = $data['params'] ?? []; 876f8e03f5SAndreas Gohr $this->version = 1; 886f8e03f5SAndreas Gohr 896f8e03f5SAndreas Gohr // always return the same ID 906f8e03f5SAndreas Gohr if (isset($data['id'])) $return['id'] = $data['id']; 916f8e03f5SAndreas Gohr 926f8e03f5SAndreas Gohr // version 2.0 request 936f8e03f5SAndreas Gohr if (isset($data['jsonrpc'])) { 946f8e03f5SAndreas Gohr $return['jsonrpc'] = $data['jsonrpc']; 956f8e03f5SAndreas Gohr $this->version = (float)$data['jsonrpc']; 966f8e03f5SAndreas Gohr } 976f8e03f5SAndreas Gohr 986f8e03f5SAndreas Gohr // version 1.1 request 996f8e03f5SAndreas Gohr if (isset($data['version'])) { 1006f8e03f5SAndreas Gohr $return['version'] = $data['version']; 1016f8e03f5SAndreas Gohr $this->version = (float)$data['version']; 1026f8e03f5SAndreas Gohr } 1036f8e03f5SAndreas Gohr } else { 1046f8e03f5SAndreas Gohr // this is a simplified request 1056f8e03f5SAndreas Gohr $method = $INPUT->server->str('PATH_INFO'); 1066f8e03f5SAndreas Gohr $method = trim($method, '/'); 1076f8e03f5SAndreas Gohr $params = $data; 1086f8e03f5SAndreas Gohr $this->version = 0; 1096f8e03f5SAndreas Gohr } 1106f8e03f5SAndreas Gohr 1116f8e03f5SAndreas Gohr // excute the method 1126f8e03f5SAndreas Gohr $return['result'] = $this->call($method, $params); 1136f8e03f5SAndreas Gohr $this->addErrorData($return); // handles non-error info 1146f8e03f5SAndreas Gohr return $return; 1156f8e03f5SAndreas Gohr } 1166f8e03f5SAndreas Gohr 1176f8e03f5SAndreas Gohr /** 1186f8e03f5SAndreas Gohr * Create an error response 1196f8e03f5SAndreas Gohr * 1206f8e03f5SAndreas Gohr * @param \Exception $exception 1216f8e03f5SAndreas Gohr * @return array 1226f8e03f5SAndreas Gohr */ 1236f8e03f5SAndreas Gohr public function returnError($exception) 1246f8e03f5SAndreas Gohr { 1256f8e03f5SAndreas Gohr $return = []; 1266f8e03f5SAndreas Gohr $this->addErrorData($return, $exception); 1276f8e03f5SAndreas Gohr return $return; 1286f8e03f5SAndreas Gohr } 1296f8e03f5SAndreas Gohr 1306f8e03f5SAndreas Gohr /** 1316f8e03f5SAndreas Gohr * Depending on the requested version, add error data to the response 1326f8e03f5SAndreas Gohr * 1336f8e03f5SAndreas Gohr * @param array $response 1346f8e03f5SAndreas Gohr * @param \Exception|null $e 1356f8e03f5SAndreas Gohr * @return void 1366f8e03f5SAndreas Gohr */ 1376f8e03f5SAndreas Gohr protected function addErrorData(&$response, $e = null) 1386f8e03f5SAndreas Gohr { 1396f8e03f5SAndreas Gohr if ($e !== null) { 1406f8e03f5SAndreas Gohr // error occured, add to response 1416f8e03f5SAndreas Gohr $response['error'] = [ 1426f8e03f5SAndreas Gohr 'code' => $e->getCode(), 1436f8e03f5SAndreas Gohr 'message' => $e->getMessage() 1446f8e03f5SAndreas Gohr ]; 1456f8e03f5SAndreas Gohr } else { 1466f8e03f5SAndreas Gohr // no error, act according to version 1476f8e03f5SAndreas Gohr if ($this->version > 0 && $this->version < 2) { 1486f8e03f5SAndreas Gohr // version 1.* wants null 1496f8e03f5SAndreas Gohr $response['error'] = null; 1506f8e03f5SAndreas Gohr } elseif ($this->version < 1) { 1516f8e03f5SAndreas Gohr // simplified version wants success 1526f8e03f5SAndreas Gohr $response['error'] = [ 1536f8e03f5SAndreas Gohr 'code' => 0, 1546f8e03f5SAndreas Gohr 'message' => 'success' 1556f8e03f5SAndreas Gohr ]; 1566f8e03f5SAndreas Gohr } 1576f8e03f5SAndreas Gohr // version 2 wants no error at all 1586f8e03f5SAndreas Gohr } 159f657e5d0SAndreas Gohr } 160f657e5d0SAndreas Gohr 161f657e5d0SAndreas Gohr /** 162f657e5d0SAndreas Gohr * Call an API method 163f657e5d0SAndreas Gohr * 164f657e5d0SAndreas Gohr * @param string $methodname 165f657e5d0SAndreas Gohr * @param array $args 166f657e5d0SAndreas Gohr * @return mixed 167f657e5d0SAndreas Gohr * @throws RemoteException 168f657e5d0SAndreas Gohr */ 169f657e5d0SAndreas Gohr public function call($methodname, $args) 170f657e5d0SAndreas Gohr { 171f657e5d0SAndreas Gohr try { 172*42e66c7aSAndreas Gohr return $this->remote->call($methodname, $args); 173f657e5d0SAndreas Gohr } catch (AccessDeniedException $e) { 174f657e5d0SAndreas Gohr if (!isset($_SERVER['REMOTE_USER'])) { 175f657e5d0SAndreas Gohr http_status(401); 176f657e5d0SAndreas Gohr throw new RemoteException("server error. not authorized to call method $methodname", -32603); 177f657e5d0SAndreas Gohr } else { 178f657e5d0SAndreas Gohr http_status(403); 179f657e5d0SAndreas Gohr throw new RemoteException("server error. forbidden to call the method $methodname", -32604); 180f657e5d0SAndreas Gohr } 181f657e5d0SAndreas Gohr } catch (RemoteException $e) { 182f657e5d0SAndreas Gohr http_status(400); 183f657e5d0SAndreas Gohr throw $e; 184f657e5d0SAndreas Gohr } 185f657e5d0SAndreas Gohr } 186f657e5d0SAndreas Gohr 187f657e5d0SAndreas Gohr /** 188f657e5d0SAndreas Gohr * @param string $data 189f657e5d0SAndreas Gohr * @return string 190f657e5d0SAndreas Gohr */ 191f657e5d0SAndreas Gohr public function toFile($data) 192f657e5d0SAndreas Gohr { 193f657e5d0SAndreas Gohr return base64_encode($data); 194f657e5d0SAndreas Gohr } 195f657e5d0SAndreas Gohr} 196