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 // Ensure we have at least one '.' in $method 96 list($type, $pluginName, /* $call */) = explode('.', $method . '.', 3); 97 if ($type === 'plugin') { 98 return $this->callPlugin($pluginName, $method, $args); 99 } 100 if ($this->coreMethodExist($method)) { 101 return $this->callCoreMethod($method, $args); 102 } 103 return $this->callCustomCallPlugin($method, $args); 104 } 105 106 /** 107 * Check existance of core methods 108 * 109 * @param string $name name of the method 110 * @return bool if method exists 111 */ 112 private function coreMethodExist($name) 113 { 114 $coreMethods = $this->getCoreMethods(); 115 return array_key_exists($name, $coreMethods); 116 } 117 118 /** 119 * Try to call custom methods provided by plugins 120 * 121 * @param string $method name of method 122 * @param array $args 123 * @return mixed 124 * @throws RemoteException if method not exists 125 */ 126 private function callCustomCallPlugin($method, $args) 127 { 128 $customCalls = $this->getCustomCallPlugins(); 129 if (!array_key_exists($method, $customCalls)) { 130 throw new RemoteException('Method does not exist', -32603); 131 } 132 $customCall = $customCalls[$method]; 133 return $this->callPlugin($customCall[0], $customCall[1], $args); 134 } 135 136 /** 137 * Returns plugin calls that are registered via RPC_CALL_ADD action 138 * 139 * @return array with pairs of custom plugin calls 140 * @triggers RPC_CALL_ADD 141 */ 142 private function getCustomCallPlugins() 143 { 144 if ($this->pluginCustomCalls === null) { 145 $data = array(); 146 Event::createAndTrigger('RPC_CALL_ADD', $data); 147 $this->pluginCustomCalls = $data; 148 } 149 return $this->pluginCustomCalls; 150 } 151 152 /** 153 * Call a plugin method 154 * 155 * @param string $pluginName 156 * @param string $method method name 157 * @param array $args 158 * @return mixed return of custom method 159 * @throws RemoteException 160 */ 161 private function callPlugin($pluginName, $method, $args) 162 { 163 $plugin = plugin_load('remote', $pluginName); 164 $methods = $this->getPluginMethods(); 165 if (!$plugin) { 166 throw new RemoteException('Method does not exist', -32603); 167 } 168 $this->checkAccess($methods[$method]); 169 $name = $this->getMethodName($methods, $method); 170 try { 171 set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1 172 return call_user_func_array(array($plugin, $name), $args); 173 } catch (\ArgumentCountError $th) { 174 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 175 } finally { 176 restore_error_handler(); 177 } 178 } 179 180 /** 181 * Call a core method 182 * 183 * @param string $method name of method 184 * @param array $args 185 * @return mixed 186 * @throws RemoteException if method not exist 187 */ 188 private function callCoreMethod($method, $args) 189 { 190 $coreMethods = $this->getCoreMethods(); 191 $this->checkAccess($coreMethods[$method]); 192 if (!isset($coreMethods[$method])) { 193 throw new RemoteException('Method does not exist', -32603); 194 } 195 $this->checkArgumentLength($coreMethods[$method], $args); 196 try { 197 set_error_handler(array($this, "argumentWarningHandler"), E_WARNING); // for PHP <7.1 198 return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args); 199 } catch (\ArgumentCountError $th) { 200 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 201 } finally { 202 restore_error_handler(); 203 } 204 } 205 206 /** 207 * Check if access should be checked 208 * 209 * @param array $methodMeta data about the method 210 * @throws AccessDeniedException 211 */ 212 private function checkAccess($methodMeta) 213 { 214 if (!isset($methodMeta['public'])) { 215 $this->forceAccess(); 216 } else { 217 if ($methodMeta['public'] == '0') { 218 $this->forceAccess(); 219 } 220 } 221 } 222 223 /** 224 * Check the number of parameters 225 * 226 * @param array $methodMeta data about the method 227 * @param array $args 228 * @throws RemoteException if wrong parameter count 229 */ 230 private function checkArgumentLength($methodMeta, $args) 231 { 232 if (count($methodMeta['args']) < count($args)) { 233 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 234 } 235 } 236 237 /** 238 * Determine the name of the real method 239 * 240 * @param array $methodMeta list of data of the methods 241 * @param string $method name of method 242 * @return string 243 */ 244 private function getMethodName($methodMeta, $method) 245 { 246 if (isset($methodMeta[$method]['name'])) { 247 return $methodMeta[$method]['name']; 248 } 249 $method = explode('.', $method); 250 return $method[count($method) - 1]; 251 } 252 253 /** 254 * Perform access check for current user 255 * 256 * @return bool true if the current user has access to remote api. 257 * @throws AccessDeniedException If remote access disabled 258 */ 259 public function hasAccess() 260 { 261 global $conf; 262 global $USERINFO; 263 /** @var \dokuwiki\Input\Input $INPUT */ 264 global $INPUT; 265 266 if (!$conf['remote']) { 267 throw new AccessDeniedException('server error. RPC server not enabled.', -32604); 268 } 269 if (trim($conf['remoteuser']) == '!!not set!!') { 270 return false; 271 } 272 if (!$conf['useacl']) { 273 return true; 274 } 275 if (trim($conf['remoteuser']) == '') { 276 return true; 277 } 278 279 return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']); 280 } 281 282 /** 283 * Requests access 284 * 285 * @return void 286 * @throws AccessDeniedException On denied access. 287 */ 288 public function forceAccess() 289 { 290 if (!$this->hasAccess()) { 291 throw new AccessDeniedException('server error. not authorized to call method', -32604); 292 } 293 } 294 295 /** 296 * Collects all the methods of the enabled Remote Plugins 297 * 298 * @return array all plugin methods. 299 * @throws RemoteException if not implemented 300 */ 301 public function getPluginMethods() 302 { 303 if ($this->pluginMethods === null) { 304 $this->pluginMethods = array(); 305 $plugins = plugin_list('remote'); 306 307 foreach ($plugins as $pluginName) { 308 /** @var RemotePlugin $plugin */ 309 $plugin = plugin_load('remote', $pluginName); 310 if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) { 311 throw new RemoteException( 312 "Plugin $pluginName does not implement dokuwiki\Plugin\DokuWiki_Remote_Plugin" 313 ); 314 } 315 316 try { 317 $methods = $plugin->_getMethods(); 318 } catch (\ReflectionException $e) { 319 throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e); 320 } 321 322 foreach ($methods as $method => $meta) { 323 $this->pluginMethods["plugin.$pluginName.$method"] = $meta; 324 } 325 } 326 } 327 return $this->pluginMethods; 328 } 329 330 /** 331 * Collects all the core methods 332 * 333 * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore 334 * instance. (for mocking) 335 * @return array all core methods. 336 */ 337 public function getCoreMethods($apiCore = null) 338 { 339 if ($this->coreMethods === null) { 340 if ($apiCore === null) { 341 $this->coreMethods = new ApiCore($this); 342 } else { 343 $this->coreMethods = $apiCore; 344 } 345 } 346 return $this->coreMethods->__getRemoteInfo(); 347 } 348 349 /** 350 * Transform file to xml 351 * 352 * @param mixed $data 353 * @return mixed 354 */ 355 public function toFile($data) 356 { 357 return call_user_func($this->fileTransformation, $data); 358 } 359 360 /** 361 * Transform date to xml 362 * 363 * @param mixed $data 364 * @return mixed 365 */ 366 public function toDate($data) 367 { 368 return call_user_func($this->dateTransformation, $data); 369 } 370 371 /** 372 * A simple transformation 373 * 374 * @param mixed $data 375 * @return mixed 376 */ 377 public function dummyTransformation($data) 378 { 379 return $data; 380 } 381 382 /** 383 * Set the transformer function 384 * 385 * @param callback $dateTransformation 386 */ 387 public function setDateTransformation($dateTransformation) 388 { 389 $this->dateTransformation = $dateTransformation; 390 } 391 392 /** 393 * Set the transformer function 394 * 395 * @param callback $fileTransformation 396 */ 397 public function setFileTransformation($fileTransformation) 398 { 399 $this->fileTransformation = $fileTransformation; 400 } 401 402 /** 403 * The error handler that catches argument-related warnings 404 */ 405 public function argumentWarningHandler($errno, $errstr) 406 { 407 if (substr($errstr, 0, 17) == 'Missing argument ') { 408 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 409 } 410 } 411} 412