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