Api.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. namespace dokuwiki\Remote;
  3. use dokuwiki\Extension\Event;
  4. use dokuwiki\Extension\RemotePlugin;
  5. /**
  6. * This class provides information about remote access to the wiki.
  7. *
  8. * == Types of methods ==
  9. * There are two types of remote methods. The first is the core methods.
  10. * These are always available and provided by dokuwiki.
  11. * The other is plugin methods. These are provided by remote plugins.
  12. *
  13. * == Information structure ==
  14. * The information about methods will be given in an array with the following structure:
  15. * array(
  16. * 'method.remoteName' => array(
  17. * 'args' => array(
  18. * 'type eg. string|int|...|date|file',
  19. * )
  20. * 'name' => 'method name in class',
  21. * 'return' => 'type',
  22. * 'public' => 1/0 - method bypass default group check (used by login)
  23. * ['doc' = 'method documentation'],
  24. * )
  25. * )
  26. *
  27. * plugin names are formed the following:
  28. * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
  29. * i.e.: dokuwiki.version or wiki.getPage
  30. *
  31. * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
  32. * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
  33. */
  34. class Api
  35. {
  36. /**
  37. * @var ApiCore
  38. */
  39. private $coreMethods = null;
  40. /**
  41. * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
  42. * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
  43. */
  44. private $pluginMethods = null;
  45. /**
  46. * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
  47. * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
  48. *
  49. * The remote method name is the same as in the remote name returned by _getMethods().
  50. */
  51. private $pluginCustomCalls = null;
  52. private $dateTransformation;
  53. private $fileTransformation;
  54. /**
  55. * constructor
  56. */
  57. public function __construct()
  58. {
  59. $this->dateTransformation = array($this, 'dummyTransformation');
  60. $this->fileTransformation = array($this, 'dummyTransformation');
  61. }
  62. /**
  63. * Get all available methods with remote access.
  64. *
  65. * @return array with information to all available methods
  66. * @throws RemoteException
  67. */
  68. public function getMethods()
  69. {
  70. return array_merge($this->getCoreMethods(), $this->getPluginMethods());
  71. }
  72. /**
  73. * Call a method via remote api.
  74. *
  75. * @param string $method name of the method to call.
  76. * @param array $args arguments to pass to the given method
  77. * @return mixed result of method call, must be a primitive type.
  78. * @throws RemoteException
  79. */
  80. public function call($method, $args = array())
  81. {
  82. if ($args === null) {
  83. $args = array();
  84. }
  85. // Ensure we have at least one '.' in $method
  86. list($type, $pluginName, /* $call */) = sexplode('.', $method . '.', 3, '');
  87. if ($type === 'plugin') {
  88. return $this->callPlugin($pluginName, $method, $args);
  89. }
  90. if ($this->coreMethodExist($method)) {
  91. return $this->callCoreMethod($method, $args);
  92. }
  93. return $this->callCustomCallPlugin($method, $args);
  94. }
  95. /**
  96. * Check existance of core methods
  97. *
  98. * @param string $name name of the method
  99. * @return bool if method exists
  100. */
  101. private function coreMethodExist($name)
  102. {
  103. $coreMethods = $this->getCoreMethods();
  104. return array_key_exists($name, $coreMethods);
  105. }
  106. /**
  107. * Try to call custom methods provided by plugins
  108. *
  109. * @param string $method name of method
  110. * @param array $args
  111. * @return mixed
  112. * @throws RemoteException if method not exists
  113. */
  114. private function callCustomCallPlugin($method, $args)
  115. {
  116. $customCalls = $this->getCustomCallPlugins();
  117. if (!array_key_exists($method, $customCalls)) {
  118. throw new RemoteException('Method does not exist', -32603);
  119. }
  120. list($plugin, $method) = $customCalls[$method];
  121. $fullMethod = "plugin.$plugin.$method";
  122. return $this->callPlugin($plugin, $fullMethod, $args);
  123. }
  124. /**
  125. * Returns plugin calls that are registered via RPC_CALL_ADD action
  126. *
  127. * @return array with pairs of custom plugin calls
  128. * @triggers RPC_CALL_ADD
  129. */
  130. private function getCustomCallPlugins()
  131. {
  132. if ($this->pluginCustomCalls === null) {
  133. $data = array();
  134. Event::createAndTrigger('RPC_CALL_ADD', $data);
  135. $this->pluginCustomCalls = $data;
  136. }
  137. return $this->pluginCustomCalls;
  138. }
  139. /**
  140. * Call a plugin method
  141. *
  142. * @param string $pluginName
  143. * @param string $method method name
  144. * @param array $args
  145. * @return mixed return of custom method
  146. * @throws RemoteException
  147. */
  148. private function callPlugin($pluginName, $method, $args)
  149. {
  150. $plugin = plugin_load('remote', $pluginName);
  151. $methods = $this->getPluginMethods();
  152. if (!$plugin) {
  153. throw new RemoteException('Method does not exist', -32603);
  154. }
  155. $this->checkAccess($methods[$method]);
  156. $name = $this->getMethodName($methods, $method);
  157. try {
  158. set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
  159. return call_user_func_array(array($plugin, $name), $args);
  160. } catch (\ArgumentCountError $th) {
  161. throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
  162. } finally {
  163. restore_error_handler();
  164. }
  165. }
  166. /**
  167. * Call a core method
  168. *
  169. * @param string $method name of method
  170. * @param array $args
  171. * @return mixed
  172. * @throws RemoteException if method not exist
  173. */
  174. private function callCoreMethod($method, $args)
  175. {
  176. $coreMethods = $this->getCoreMethods();
  177. $this->checkAccess($coreMethods[$method]);
  178. if (!isset($coreMethods[$method])) {
  179. throw new RemoteException('Method does not exist', -32603);
  180. }
  181. $this->checkArgumentLength($coreMethods[$method], $args);
  182. try {
  183. set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1
  184. return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args);
  185. } catch (\ArgumentCountError $th) {
  186. throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
  187. } finally {
  188. restore_error_handler();
  189. }
  190. }
  191. /**
  192. * Check if access should be checked
  193. *
  194. * @param array $methodMeta data about the method
  195. * @throws AccessDeniedException
  196. */
  197. private function checkAccess($methodMeta)
  198. {
  199. if (!isset($methodMeta['public'])) {
  200. $this->forceAccess();
  201. } else {
  202. if ($methodMeta['public'] == '0') {
  203. $this->forceAccess();
  204. }
  205. }
  206. }
  207. /**
  208. * Check the number of parameters
  209. *
  210. * @param array $methodMeta data about the method
  211. * @param array $args
  212. * @throws RemoteException if wrong parameter count
  213. */
  214. private function checkArgumentLength($methodMeta, $args)
  215. {
  216. if (count($methodMeta['args']) < count($args)) {
  217. throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
  218. }
  219. }
  220. /**
  221. * Determine the name of the real method
  222. *
  223. * @param array $methodMeta list of data of the methods
  224. * @param string $method name of method
  225. * @return string
  226. */
  227. private function getMethodName($methodMeta, $method)
  228. {
  229. if (isset($methodMeta[$method]['name'])) {
  230. return $methodMeta[$method]['name'];
  231. }
  232. $method = explode('.', $method);
  233. return $method[count($method) - 1];
  234. }
  235. /**
  236. * Perform access check for current user
  237. *
  238. * @return bool true if the current user has access to remote api.
  239. * @throws AccessDeniedException If remote access disabled
  240. */
  241. public function hasAccess()
  242. {
  243. global $conf;
  244. global $USERINFO;
  245. /** @var \dokuwiki\Input\Input $INPUT */
  246. global $INPUT;
  247. if (!$conf['remote']) {
  248. throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
  249. }
  250. if (trim($conf['remoteuser']) == '!!not set!!') {
  251. return false;
  252. }
  253. if (!$conf['useacl']) {
  254. return true;
  255. }
  256. if (trim($conf['remoteuser']) == '') {
  257. return true;
  258. }
  259. return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
  260. }
  261. /**
  262. * Requests access
  263. *
  264. * @return void
  265. * @throws AccessDeniedException On denied access.
  266. */
  267. public function forceAccess()
  268. {
  269. if (!$this->hasAccess()) {
  270. throw new AccessDeniedException('server error. not authorized to call method', -32604);
  271. }
  272. }
  273. /**
  274. * Collects all the methods of the enabled Remote Plugins
  275. *
  276. * @return array all plugin methods.
  277. * @throws RemoteException if not implemented
  278. */
  279. public function getPluginMethods()
  280. {
  281. if ($this->pluginMethods === null) {
  282. $this->pluginMethods = array();
  283. $plugins = plugin_list('remote');
  284. foreach ($plugins as $pluginName) {
  285. /** @var RemotePlugin $plugin */
  286. $plugin = plugin_load('remote', $pluginName);
  287. if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
  288. throw new RemoteException(
  289. "Plugin $pluginName does not implement dokuwiki\Plugin\DokuWiki_Remote_Plugin"
  290. );
  291. }
  292. try {
  293. $methods = $plugin->_getMethods();
  294. } catch (\ReflectionException $e) {
  295. throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
  296. }
  297. foreach ($methods as $method => $meta) {
  298. $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
  299. }
  300. }
  301. }
  302. return $this->pluginMethods;
  303. }
  304. /**
  305. * Collects all the core methods
  306. *
  307. * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore
  308. * instance. (for mocking)
  309. * @return array all core methods.
  310. */
  311. public function getCoreMethods($apiCore = null)
  312. {
  313. if ($this->coreMethods === null) {
  314. if ($apiCore === null) {
  315. $this->coreMethods = new ApiCore($this);
  316. } else {
  317. $this->coreMethods = $apiCore;
  318. }
  319. }
  320. return $this->coreMethods->getRemoteInfo();
  321. }
  322. /**
  323. * Transform file to xml
  324. *
  325. * @param mixed $data
  326. * @return mixed
  327. */
  328. public function toFile($data)
  329. {
  330. return call_user_func($this->fileTransformation, $data);
  331. }
  332. /**
  333. * Transform date to xml
  334. *
  335. * @param mixed $data
  336. * @return mixed
  337. */
  338. public function toDate($data)
  339. {
  340. return call_user_func($this->dateTransformation, $data);
  341. }
  342. /**
  343. * A simple transformation
  344. *
  345. * @param mixed $data
  346. * @return mixed
  347. */
  348. public function dummyTransformation($data)
  349. {
  350. return $data;
  351. }
  352. /**
  353. * Set the transformer function
  354. *
  355. * @param callback $dateTransformation
  356. */
  357. public function setDateTransformation($dateTransformation)
  358. {
  359. $this->dateTransformation = $dateTransformation;
  360. }
  361. /**
  362. * Set the transformer function
  363. *
  364. * @param callback $fileTransformation
  365. */
  366. public function setFileTransformation($fileTransformation)
  367. {
  368. $this->fileTransformation = $fileTransformation;
  369. }
  370. /**
  371. * The error handler that catches argument-related warnings
  372. */
  373. public function argumentWarningHandler($errno, $errstr)
  374. {
  375. if (substr($errstr, 0, 17) == 'Missing argument ') {
  376. throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
  377. }
  378. }
  379. }