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