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