1<?php 2 3namespace dokuwiki\Remote; 4 5/** 6 * Provide the Remote XMLRPC API as a JSON based API 7 */ 8class JsonRpcServer 9{ 10 protected $remote; 11 12 /** @var float The XML-RPC Version. 0 is our own simplified variant */ 13 protected $version = 0; 14 15 /** 16 * JsonRpcServer constructor. 17 */ 18 public function __construct() 19 { 20 $this->remote = new Api(); 21 } 22 23 /** 24 * Serve the request 25 * 26 * @param string $body Should only be set for testing, otherwise the request body is read from php://input 27 * @return mixed 28 * @throws RemoteException 29 */ 30 public function serve($body = '') 31 { 32 global $conf; 33 global $INPUT; 34 35 if (!$conf['remote']) { 36 http_status(404); 37 throw new RemoteException("JSON-RPC server not enabled.", -32605); 38 } 39 if (!empty($conf['remotecors'])) { 40 header('Access-Control-Allow-Origin: ' . $conf['remotecors']); 41 } 42 if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') { 43 http_status(405); 44 header('Allow: POST'); 45 throw new RemoteException("JSON-RPC server only accepts POST requests.", -32606); 46 } 47 if ($INPUT->server->str('CONTENT_TYPE') !== 'application/json') { 48 http_status(415); 49 throw new RemoteException("JSON-RPC server only accepts application/json requests.", -32606); 50 } 51 52 try { 53 if ($body === '') { 54 $body = file_get_contents('php://input'); 55 } 56 if ($body !== '') { 57 $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); 58 } else { 59 $data = []; 60 } 61 } catch (\Exception $e) { 62 http_status(400); 63 throw new RemoteException("JSON-RPC server only accepts valid JSON.", -32700); 64 } 65 66 return $this->createResponse($data); 67 } 68 69 /** 70 * This executes the method and returns the result 71 * 72 * This should handle all JSON-RPC versions and our simplified version 73 * 74 * @link https://en.wikipedia.org/wiki/JSON-RPC 75 * @link https://www.jsonrpc.org/specification 76 * @param array $data 77 * @return array 78 * @throws RemoteException 79 */ 80 protected function createResponse($data) 81 { 82 global $INPUT; 83 $return = []; 84 85 if (isset($data['method'])) { 86 // this is a standard conform request (at least version 1.0) 87 $method = $data['method']; 88 $params = $data['params'] ?? []; 89 $this->version = 1; 90 91 // always return the same ID 92 if (isset($data['id'])) $return['id'] = $data['id']; 93 94 // version 2.0 request 95 if (isset($data['jsonrpc'])) { 96 $return['jsonrpc'] = $data['jsonrpc']; 97 $this->version = (float)$data['jsonrpc']; 98 } 99 100 // version 1.1 request 101 if (isset($data['version'])) { 102 $return['version'] = $data['version']; 103 $this->version = (float)$data['version']; 104 } 105 } else { 106 // this is a simplified request 107 $method = $INPUT->server->str('PATH_INFO'); 108 $method = trim($method, '/'); 109 $params = $data; 110 $this->version = 0; 111 } 112 113 // excute the method 114 $return['result'] = $this->call($method, $params); 115 $this->addErrorData($return); // handles non-error info 116 return $return; 117 } 118 119 /** 120 * Create an error response 121 * 122 * @param \Exception $exception 123 * @return array 124 */ 125 public function returnError($exception) 126 { 127 $return = []; 128 $this->addErrorData($return, $exception); 129 return $return; 130 } 131 132 /** 133 * Depending on the requested version, add error data to the response 134 * 135 * @param array $response 136 * @param \Exception|null $e 137 * @return void 138 */ 139 protected function addErrorData(&$response, $e = null) 140 { 141 if ($e !== null) { 142 // error occured, add to response 143 $response['error'] = [ 144 'code' => $e->getCode(), 145 'message' => $e->getMessage() 146 ]; 147 } else { 148 // no error, act according to version 149 if ($this->version > 0 && $this->version < 2) { 150 // version 1.* wants null 151 $response['error'] = null; 152 } elseif ($this->version < 1) { 153 // simplified version wants success 154 $response['error'] = [ 155 'code' => 0, 156 'message' => 'success' 157 ]; 158 } 159 // version 2 wants no error at all 160 } 161 } 162 163 /** 164 * Call an API method 165 * 166 * @param string $methodname 167 * @param array $args 168 * @return mixed 169 * @throws RemoteException 170 */ 171 public function call($methodname, $args) 172 { 173 try { 174 return $this->remote->call($methodname, $args); 175 } catch (AccessDeniedException $e) { 176 if (!isset($_SERVER['REMOTE_USER'])) { 177 http_status(401); 178 throw new RemoteException("server error. not authorized to call method $methodname", -32603); 179 } else { 180 http_status(403); 181 throw new RemoteException("server error. forbidden to call the method $methodname", -32604); 182 } 183 } catch (RemoteException $e) { 184 http_status(400); 185 throw $e; 186 } 187 } 188} 189