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