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