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