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