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