[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 3 namespace dokuwiki\Remote; 4 5 use dokuwiki\Extension\PluginInterface; 6 use dokuwiki\Input\Input; 7 use dokuwiki\Extension\Event; 8 use dokuwiki\Extension\RemotePlugin; 9 10 /** 11 * This class provides information about remote access to the wiki. 12 * 13 * == Types of methods == 14 * There are two types of remote methods. The first is the core methods. 15 * These are always available and provided by dokuwiki. 16 * The other is plugin methods. These are provided by remote plugins. 17 * 18 * == Information structure == 19 * The information about methods will be given in an array with the following structure: 20 * array( 21 * 'method.remoteName' => array( 22 * 'args' => array( 23 * 'type eg. string|int|...|date|file', 24 * ) 25 * 'name' => 'method name in class', 26 * 'return' => 'type', 27 * 'public' => 1/0 - method bypass default group check (used by login) 28 * ['doc' = 'method documentation'], 29 * ) 30 * ) 31 * 32 * plugin names are formed the following: 33 * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself. 34 * i.e.: dokuwiki.version or wiki.getPage 35 * 36 * plugin methods are formed like 'plugin.<plugin name>.<method name>'. 37 * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime 38 */ 39 class Api 40 { 41 /** 42 * @var ApiCore|\RemoteAPICoreTest 43 */ 44 private $coreMethods; 45 46 /** 47 * @var array remote methods provided by dokuwiki plugins - will be filled lazy via 48 * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods} 49 */ 50 private $pluginMethods; 51 52 /** 53 * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event. 54 * The data inside is 'custom.call.something' => array('plugin name', 'remote method name') 55 * 56 * The remote method name is the same as in the remote name returned by _getMethods(). 57 */ 58 private $pluginCustomCalls; 59 60 private $dateTransformation; 61 private $fileTransformation; 62 63 /** 64 * constructor 65 */ 66 public function __construct() 67 { 68 $this->dateTransformation = [$this, 'dummyTransformation']; 69 $this->fileTransformation = [$this, 'dummyTransformation']; 70 } 71 72 /** 73 * Get all available methods with remote access. 74 * 75 * @return array with information to all available methods 76 * @throws RemoteException 77 */ 78 public function getMethods() 79 { 80 return array_merge($this->getCoreMethods(), $this->getPluginMethods()); 81 } 82 83 /** 84 * Call a method via remote api. 85 * 86 * @param string $method name of the method to call. 87 * @param array $args arguments to pass to the given method 88 * @return mixed result of method call, must be a primitive type. 89 * @throws RemoteException 90 */ 91 public function call($method, $args = []) 92 { 93 if ($args === null) { 94 $args = []; 95 } 96 // Ensure we have at least one '.' in $method 97 [$type, $pluginName, /* call */] = sexplode('.', $method . '.', 3, ''); 98 if ($type === 'plugin') { 99 return $this->callPlugin($pluginName, $method, $args); 100 } 101 if ($this->coreMethodExist($method)) { 102 return $this->callCoreMethod($method, $args); 103 } 104 return $this->callCustomCallPlugin($method, $args); 105 } 106 107 /** 108 * Check existance of core methods 109 * 110 * @param string $name name of the method 111 * @return bool if method exists 112 */ 113 private function coreMethodExist($name) 114 { 115 $coreMethods = $this->getCoreMethods(); 116 return array_key_exists($name, $coreMethods); 117 } 118 119 /** 120 * Try to call custom methods provided by plugins 121 * 122 * @param string $method name of method 123 * @param array $args 124 * @return mixed 125 * @throws RemoteException if method not exists 126 */ 127 private function callCustomCallPlugin($method, $args) 128 { 129 $customCalls = $this->getCustomCallPlugins(); 130 if (!array_key_exists($method, $customCalls)) { 131 throw new RemoteException('Method does not exist', -32603); 132 } 133 [$plugin, $method] = $customCalls[$method]; 134 $fullMethod = "plugin.$plugin.$method"; 135 return $this->callPlugin($plugin, $fullMethod, $args); 136 } 137 138 /** 139 * Returns plugin calls that are registered via RPC_CALL_ADD action 140 * 141 * @return array with pairs of custom plugin calls 142 * @triggers RPC_CALL_ADD 143 */ 144 private function getCustomCallPlugins() 145 { 146 if ($this->pluginCustomCalls === null) { 147 $data = []; 148 Event::createAndTrigger('RPC_CALL_ADD', $data); 149 $this->pluginCustomCalls = $data; 150 } 151 return $this->pluginCustomCalls; 152 } 153 154 /** 155 * Call a plugin method 156 * 157 * @param string $pluginName 158 * @param string $method method name 159 * @param array $args 160 * @return mixed return of custom method 161 * @throws RemoteException 162 */ 163 private function callPlugin($pluginName, $method, $args) 164 { 165 $plugin = plugin_load('remote', $pluginName); 166 $methods = $this->getPluginMethods(); 167 if (!$plugin instanceof PluginInterface) { 168 throw new RemoteException('Method does not exist', -32603); 169 } 170 $this->checkAccess($methods[$method]); 171 $name = $this->getMethodName($methods, $method); 172 try { 173 set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1 174 return call_user_func_array([$plugin, $name], $args); 175 } catch (\ArgumentCountError $th) { 176 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 177 } finally { 178 restore_error_handler(); 179 } 180 } 181 182 /** 183 * Call a core method 184 * 185 * @param string $method name of method 186 * @param array $args 187 * @return mixed 188 * @throws RemoteException if method not exist 189 */ 190 private function callCoreMethod($method, $args) 191 { 192 $coreMethods = $this->getCoreMethods(); 193 $this->checkAccess($coreMethods[$method]); 194 if (!isset($coreMethods[$method])) { 195 throw new RemoteException('Method does not exist', -32603); 196 } 197 $this->checkArgumentLength($coreMethods[$method], $args); 198 try { 199 set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1 200 return call_user_func_array([$this->coreMethods, $this->getMethodName($coreMethods, $method)], $args); 201 } catch (\ArgumentCountError $th) { 202 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 203 } finally { 204 restore_error_handler(); 205 } 206 } 207 208 /** 209 * Check if access should be checked 210 * 211 * @param array $methodMeta data about the method 212 * @throws AccessDeniedException 213 */ 214 private function checkAccess($methodMeta) 215 { 216 if (!isset($methodMeta['public'])) { 217 $this->forceAccess(); 218 } elseif ($methodMeta['public'] == '0') { 219 $this->forceAccess(); 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 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 = []; 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\Extension\RemotePlugin" 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|\RemoteAPICoreTest $apiCore this parameter is used for testing. 334 * Here you can pass a non-default RemoteAPICore 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 (str_starts_with($errstr, 'Missing argument ')) { 408 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 409 } 410 } 411 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body