You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

189 lines
5.4 KiB

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