gggeek /
phpxmlrpc
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * @author Gaetano Giunta |
||
| 4 | * @copyright (C) 2006-2025 G. Giunta |
||
| 5 | * @license code licensed under the BSD License: see file license.txt |
||
| 6 | */ |
||
| 7 | |||
| 8 | namespace PhpXmlRpc; |
||
| 9 | |||
| 10 | use PhpXmlRpc\Exception\ValueErrorException; |
||
| 11 | use PhpXmlRpc\Traits\LoggerAware; |
||
| 12 | |||
| 13 | /** |
||
| 14 | * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa. |
||
| 15 | * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies. |
||
| 16 | * |
||
| 17 | * @todo use some better templating system for code generation? |
||
| 18 | * @todo implement method wrapping with preservation of php objs in calls |
||
| 19 | * @todo add support for 'epivals' mode |
||
| 20 | * @todo allow setting custom namespace for generated wrapping code |
||
| 21 | */ |
||
| 22 | class Wrapper |
||
| 23 | { |
||
| 24 | use LoggerAware; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * @var object[] |
||
| 28 | * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode |
||
| 29 | 24 | * @internal this property will become protected in the future |
|
| 30 | */ |
||
| 31 | 24 | public static $objHolder = array(); |
|
| 32 | 1 | ||
| 33 | /** @var string */ |
||
| 34 | 24 | protected static $namespace = '\\PhpXmlRpc\\'; |
|
| 35 | |||
| 36 | /** @var string */ |
||
| 37 | protected static $prefix = 'xmlrpc'; |
||
| 38 | |||
| 39 | /** @var null|string set to a namespaced class. If empty, static::$namespace . 'Response' will be used */ |
||
| 40 | protected static $allowedResponseClass = null; |
||
| 41 | |||
| 42 | /** |
||
| 43 | * Given a string defining a php type or phpxmlrpc type (loosely defined: strings |
||
| 44 | * accepted come from javadoc blocks), return corresponding phpxmlrpc type. |
||
| 45 | * Notes: |
||
| 46 | * - for php 'resource' types returns empty string, since resources cannot be serialized; |
||
| 47 | * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs |
||
| 48 | * - for php arrays always return array, even though arrays sometimes serialize as structs... |
||
| 49 | * - for 'void' and 'null' returns 'undefined' |
||
| 50 | * |
||
| 51 | * @param string $phpType |
||
| 52 | * @return string |
||
| 53 | * |
||
| 54 | * @todo support notation `something[]` as 'array' |
||
| 55 | * @todo check if nil support is enabled when finding null or void (which makes sense in php for return type) |
||
| 56 | */ |
||
| 57 | 559 | public function php2XmlrpcType($phpType) |
|
| 58 | { |
||
| 59 | 559 | switch (strtolower($phpType)) { |
|
| 60 | 559 | case 'string': |
|
| 61 | 559 | return Value::$xmlrpcString; |
|
| 62 | 559 | case 'integer': |
|
| 63 | 559 | case Value::$xmlrpcInt: // 'int' |
|
| 64 | 559 | case Value::$xmlrpcI4: |
|
| 65 | 559 | case Value::$xmlrpcI8: |
|
| 66 | 559 | return Value::$xmlrpcInt; |
|
| 67 | 559 | case Value::$xmlrpcDouble: // 'double' |
|
| 68 | return Value::$xmlrpcDouble; |
||
| 69 | 559 | case 'bool': |
|
| 70 | 559 | case Value::$xmlrpcBoolean: // 'boolean' |
|
| 71 | 559 | case 'false': |
|
| 72 | 559 | case 'true': |
|
| 73 | return Value::$xmlrpcBoolean; |
||
| 74 | 559 | case Value::$xmlrpcArray: // 'array': |
|
| 75 | 559 | case 'array[]': |
|
| 76 | return Value::$xmlrpcArray; |
||
| 77 | 559 | case 'object': |
|
| 78 | 559 | case Value::$xmlrpcStruct: // 'struct' |
|
| 79 | return Value::$xmlrpcStruct; |
||
| 80 | 559 | case Value::$xmlrpcBase64: |
|
| 81 | return Value::$xmlrpcBase64; |
||
| 82 | 559 | case 'resource': |
|
| 83 | return ''; |
||
| 84 | default: |
||
| 85 | 559 | if (class_exists($phpType)) { |
|
| 86 | 559 | // DateTimeInterface is not present in php 5.4... |
|
| 87 | if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) { |
||
| 88 | return Value::$xmlrpcDateTime; |
||
| 89 | 559 | } |
|
| 90 | return Value::$xmlrpcStruct; |
||
| 91 | } else { |
||
| 92 | 559 | // unknown: might be any 'extended' xml-rpc type |
|
| 93 | return Value::$xmlrpcValue; |
||
| 94 | } |
||
| 95 | } |
||
| 96 | } |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Given a string defining a phpxmlrpc type return the corresponding php type. |
||
| 100 | * |
||
| 101 | * @param string $xmlrpcType |
||
| 102 | * @return string |
||
| 103 | */ |
||
| 104 | 85 | public function xmlrpc2PhpType($xmlrpcType) |
|
| 105 | { |
||
| 106 | 85 | switch (strtolower($xmlrpcType)) { |
|
| 107 | 85 | case 'base64': |
|
| 108 | 85 | case 'datetime.iso8601': |
|
| 109 | 85 | case 'string': |
|
| 110 | 64 | return Value::$xmlrpcString; |
|
| 111 | 85 | case 'int': |
|
| 112 | 22 | case 'i4': |
|
| 113 | 22 | case 'i8': |
|
| 114 | 64 | return 'integer'; |
|
| 115 | 22 | case 'struct': |
|
| 116 | 22 | case 'array': |
|
| 117 | return 'array'; |
||
| 118 | 22 | case 'double': |
|
| 119 | return 'float'; |
||
| 120 | 22 | case 'undefined': |
|
| 121 | 22 | return 'mixed'; |
|
| 122 | case 'boolean': |
||
| 123 | case 'null': |
||
| 124 | default: |
||
| 125 | // unknown: might be any xml-rpc type |
||
| 126 | return strtolower($xmlrpcType); |
||
| 127 | } |
||
| 128 | } |
||
| 129 | |||
| 130 | /** |
||
| 131 | * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an |
||
| 132 | * xml-rpc server object and called from remote clients (as well as its corresponding signature info). |
||
| 133 | * |
||
| 134 | * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the |
||
| 135 | * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as |
||
| 136 | * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to |
||
| 137 | * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by |
||
| 138 | * the lib, while datetime values are passed around as strings) |
||
| 139 | * |
||
| 140 | * Known limitations: |
||
| 141 | * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving |
||
| 142 | * number/type of params for those) |
||
| 143 | * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of |
||
| 144 | * those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php |
||
| 145 | * objects will be rebuilt. |
||
| 146 | * In short: php objects can be serialized, too (except for their resource members), using this function. |
||
| 147 | * Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard |
||
| 148 | * attribute on struct element tags) |
||
| 149 | * |
||
| 150 | * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions |
||
| 151 | * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and |
||
| 152 | * $exception_handling properties. |
||
| 153 | * |
||
| 154 | * @param \Callable $callable the PHP user function to be exposed as xml-rpc method: a closure, function name, array($obj, 'methodname') or array('class', 'methodname') are ok |
||
| 155 | * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true |
||
| 156 | * @param array $extraOptions (optional) array of options for conversion. valid values include: |
||
| 157 | * - bool return_source when true, php code w. function definition will be returned, instead of a closure |
||
| 158 | * - bool encode_nulls let php objects be sent to server using <nil> elements instead of empty strings |
||
| 159 | * - bool encode_php_objs let php objects be sent to server using the 'improved' xml-rpc notation, so server can deserialize them as php objects |
||
| 160 | * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with trusted servers --- |
||
| 161 | * - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked |
||
| 162 | * @return array|false false on error, or an array containing the name of the new php function, |
||
| 163 | * its signature and docs, to be used in the server dispatch map |
||
| 164 | * |
||
| 165 | * @todo decide how to deal with params passed by ref in function definition: bomb out or allow? |
||
| 166 | * @todo finish using phpdoc info to build method sig if all params are named but out of order |
||
| 167 | * @todo add a check for params of 'resource' type |
||
| 168 | * @todo add some error logging when returning false? |
||
| 169 | * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value... |
||
| 170 | * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3? |
||
| 171 | * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance? |
||
| 172 | * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway |
||
| 173 | */ |
||
| 174 | public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array()) |
||
| 175 | { |
||
| 176 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
||
| 177 | 559 | ||
| 178 | if (is_string($callable) && strpos($callable, '::') !== false) { |
||
| 179 | 559 | $callable = explode('::', $callable); |
|
| 180 | } |
||
| 181 | 559 | if (is_array($callable)) { |
|
| 182 | 559 | if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) { |
|
| 183 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong'); |
||
| 184 | 559 | return false; |
|
| 185 | 559 | } |
|
| 186 | if (is_string($callable[0])) { |
||
| 187 | $plainFuncName = implode('::', $callable); |
||
| 188 | } elseif (is_object($callable[0])) { |
||
| 189 | 559 | $plainFuncName = get_class($callable[0]) . '->' . $callable[1]; |
|
| 190 | 559 | } |
|
| 191 | 559 | $exists = method_exists($callable[0], $callable[1]); |
|
| 192 | 559 | } else if ($callable instanceof \Closure) { |
|
| 193 | // we do not support creating code which wraps closures, as php does not allow to serialize them |
||
| 194 | 559 | if (!$buildIt) { |
|
| 195 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code'); |
|
| 196 | return false; |
||
| 197 | 559 | } |
|
| 198 | |||
| 199 | $plainFuncName = 'Closure'; |
||
| 200 | $exists = true; |
||
| 201 | } else { |
||
| 202 | 559 | $plainFuncName = $callable; |
|
| 203 | 559 | $exists = function_exists($callable); |
|
| 204 | } |
||
| 205 | 559 | ||
| 206 | 559 | if (!$exists) { |
|
| 207 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName); |
||
| 208 | return false; |
||
| 209 | 559 | } |
|
| 210 | |||
| 211 | $funcDesc = $this->introspectFunction($callable, $plainFuncName); |
||
| 212 | if (!$funcDesc) { |
||
| 213 | return false; |
||
| 214 | 559 | } |
|
| 215 | 559 | ||
| 216 | $funcSigs = $this->buildMethodSignatures($funcDesc); |
||
| 217 | |||
| 218 | if ($buildIt) { |
||
| 219 | 559 | $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc); |
|
| 220 | } else { |
||
| 221 | 559 | $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions); |
|
| 222 | 559 | $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc); |
|
| 223 | } |
||
| 224 | 559 | ||
| 225 | 559 | $ret = array( |
|
| 226 | 'function' => $callable, |
||
| 227 | 'signature' => $funcSigs['sigs'], |
||
| 228 | 'docstring' => $funcDesc['desc'], |
||
| 229 | 559 | 'signature_docs' => $funcSigs['sigsDocs'], |
|
| 230 | 559 | ); |
|
| 231 | 559 | if (!$buildIt) { |
|
| 232 | 559 | $ret['function'] = $newFuncName; |
|
| 233 | $ret['source'] = $code; |
||
| 234 | 559 | } |
|
| 235 | 559 | return $ret; |
|
| 236 | 559 | } |
|
| 237 | |||
| 238 | 559 | /** |
|
| 239 | * Introspect a php callable and its phpdoc block and extract information about its signature |
||
| 240 | * |
||
| 241 | * @param callable $callable |
||
| 242 | * @param string $plainFuncName |
||
| 243 | * @return array|false |
||
| 244 | */ |
||
| 245 | protected function introspectFunction($callable, $plainFuncName) |
||
| 246 | { |
||
| 247 | // start to introspect PHP code |
||
| 248 | 559 | if (is_array($callable)) { |
|
| 249 | $func = new \ReflectionMethod($callable[0], $callable[1]); |
||
| 250 | if ($func->isPrivate()) { |
||
| 251 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName); |
|
| 252 | 559 | return false; |
|
| 253 | 559 | } |
|
| 254 | if ($func->isProtected()) { |
||
| 255 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName); |
||
| 256 | return false; |
||
| 257 | 559 | } |
|
| 258 | if ($func->isConstructor()) { |
||
| 259 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName); |
||
| 260 | return false; |
||
| 261 | 559 | } |
|
| 262 | if ($func->isDestructor()) { |
||
| 263 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName); |
||
| 264 | return false; |
||
| 265 | 559 | } |
|
| 266 | if ($func->isAbstract()) { |
||
| 267 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName); |
||
| 268 | return false; |
||
| 269 | 559 | } |
|
| 270 | /// @todo add more checks for static vs. nonstatic? |
||
| 271 | } else { |
||
| 272 | $func = new \ReflectionFunction($callable); |
||
| 273 | } |
||
| 274 | if ($func->isInternal()) { |
||
| 275 | 559 | /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully |
|
| 276 | /// reflect internal php functions |
||
| 277 | 559 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName); |
|
| 278 | return false; |
||
| 279 | } |
||
| 280 | |||
| 281 | // retrieve parameter names, types and description from javadoc comments |
||
| 282 | |||
| 283 | // function description |
||
| 284 | $desc = ''; |
||
| 285 | // type of return val: by default 'any' |
||
| 286 | $returns = Value::$xmlrpcValue; |
||
| 287 | 559 | // desc of return val |
|
| 288 | $returnsDocs = ''; |
||
| 289 | 559 | // type + name of function parameters |
|
| 290 | $paramDocs = array(); |
||
| 291 | 559 | ||
| 292 | $docs = $func->getDocComment(); |
||
| 293 | 559 | if ($docs != '') { |
|
| 294 | $docs = explode("\n", $docs); |
||
| 295 | 559 | $i = 0; |
|
| 296 | 559 | foreach ($docs as $doc) { |
|
| 297 | 559 | $doc = trim($doc, " \r\t/*"); |
|
| 298 | 559 | if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) { |
|
| 299 | 559 | if ($desc) { |
|
| 300 | 559 | $desc .= "\n"; |
|
| 301 | 559 | } |
|
| 302 | 559 | $desc .= $doc; |
|
| 303 | 559 | } elseif (strpos($doc, '@param') === 0) { |
|
| 304 | // syntax: @param type $name [desc] |
||
| 305 | 559 | if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) { |
|
| 306 | 559 | $name = strtolower(trim($matches[2])); |
|
| 307 | //$paramDocs[$name]['name'] = trim($matches[2]); |
||
| 308 | 559 | $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : ''; |
|
| 309 | 559 | $paramDocs[$name]['type'] = $matches[1]; |
|
| 310 | } |
||
| 311 | 559 | $i++; |
|
| 312 | 559 | } elseif (strpos($doc, '@return') === 0) { |
|
| 313 | // syntax: @return type [desc] |
||
| 314 | 559 | if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) { |
|
| 315 | 559 | $returns = $matches[1]; |
|
| 316 | if (isset($matches[2])) { |
||
| 317 | 559 | $returnsDocs = trim($matches[2]); |
|
| 318 | 559 | } |
|
| 319 | 559 | } |
|
| 320 | 559 | } |
|
| 321 | } |
||
| 322 | } |
||
| 323 | |||
| 324 | // for php 7+, we can take advantage of type declarations! |
||
| 325 | if (method_exists($func, 'getReturnType')) { |
||
| 326 | $returnType = $func->getReturnType(); |
||
| 327 | if ($returnType !== null) { |
||
| 328 | 559 | /// @todo |
|
| 329 | 559 | } |
|
| 330 | 559 | } |
|
| 331 | 559 | ||
| 332 | 559 | // execute introspection of actual function prototype |
|
| 333 | 559 | $params = array(); |
|
| 334 | 559 | $i = 0; |
|
| 335 | foreach ($func->getParameters() as $paramObj) { |
||
| 336 | $params[$i] = array(); |
||
| 337 | $params[$i]['name'] = '$' . $paramObj->getName(); |
||
| 338 | 559 | $params[$i]['isoptional'] = $paramObj->isOptional(); |
|
| 339 | 559 | if (method_exists($paramObj, 'getType')) { |
|
| 340 | 559 | $paramType = $paramObj->getType(); |
|
| 341 | 559 | if ($paramType !== null) { |
|
| 342 | 559 | /// @todo |
|
| 343 | 559 | } |
|
| 344 | } |
||
| 345 | $i++; |
||
| 346 | } |
||
| 347 | |||
| 348 | return array( |
||
| 349 | 'desc' => $desc, |
||
| 350 | 'docs' => $docs, |
||
| 351 | 'params' => $params, // array, positionally indexed |
||
| 352 | 'paramDocs' => $paramDocs, // array, indexed by name |
||
| 353 | 'returns' => $returns, |
||
| 354 | 'returnsDocs' =>$returnsDocs, |
||
| 355 | ); |
||
| 356 | } |
||
| 357 | 559 | ||
| 358 | /** |
||
| 359 | 559 | * Given the method description given by introspection, create method signature data |
|
| 360 | 559 | * |
|
| 361 | 559 | * @param array $funcDesc as generated by self::introspectFunction() |
|
| 362 | 559 | * @return array |
|
| 363 | 559 | * |
|
| 364 | * @todo support better docs with multiple types separated by pipes by creating multiple signatures |
||
| 365 | * (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences, |
||
| 366 | * but it makes a lot of sense in a php >= 8 world) |
||
| 367 | */ |
||
| 368 | protected function buildMethodSignatures($funcDesc) |
||
| 369 | { |
||
| 370 | $i = 0; |
||
| 371 | $parsVariations = array(); |
||
| 372 | $pars = array(); |
||
| 373 | 559 | $pNum = count($funcDesc['params']); |
|
| 374 | foreach ($funcDesc['params'] as $param) { |
||
| 375 | /* // match by name real param and documented params |
||
| 376 | $name = strtolower($param['name']); |
||
| 377 | if (!isset($funcDesc['paramDocs'][$name])) { |
||
| 378 | 559 | $funcDesc['paramDocs'][$name] = array(); |
|
| 379 | 559 | } |
|
| 380 | 559 | if (!isset($funcDesc['paramDocs'][$name]['type'])) { |
|
| 381 | $funcDesc['paramDocs'][$name]['type'] = 'mixed'; |
||
| 382 | 559 | }*/ |
|
| 383 | |||
| 384 | if ($param['isoptional']) { |
||
| 385 | // this particular parameter is optional. save as valid previous list of parameters |
||
| 386 | 559 | $parsVariations[] = $pars; |
|
| 387 | } |
||
| 388 | 559 | ||
| 389 | $pars[] = "\$p$i"; |
||
| 390 | $i++; |
||
| 391 | 559 | if ($i == $pNum) { |
|
| 392 | 559 | // last allowed parameters combination |
|
| 393 | 559 | $parsVariations[] = $pars; |
|
| 394 | } |
||
| 395 | 559 | } |
|
| 396 | 559 | ||
| 397 | 559 | if (count($parsVariations) == 0) { |
|
| 398 | 559 | // only known good synopsis = no parameters |
|
| 399 | 559 | $parsVariations[] = array(); |
|
| 400 | 559 | } |
|
| 401 | |||
| 402 | 559 | $sigs = array(); |
|
| 403 | $sigsDocs = array(); |
||
| 404 | 559 | foreach ($parsVariations as $pars) { |
|
| 405 | // build a signature |
||
| 406 | 559 | $sig = array($this->php2XmlrpcType($funcDesc['returns'])); |
|
| 407 | 559 | $pSig = array($funcDesc['returnsDocs']); |
|
| 408 | for ($i = 0; $i < count($pars); $i++) { |
||
|
0 ignored issues
–
show
|
|||
| 409 | $name = strtolower($funcDesc['params'][$i]['name']); |
||
| 410 | if (isset($funcDesc['paramDocs'][$name]['type'])) { |
||
| 411 | 559 | $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']); |
|
| 412 | 559 | } else { |
|
| 413 | $sig[] = Value::$xmlrpcValue; |
||
| 414 | } |
||
| 415 | $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : ''; |
||
| 416 | } |
||
| 417 | $sigs[] = $sig; |
||
| 418 | $sigsDocs[] = $pSig; |
||
| 419 | } |
||
| 420 | |||
| 421 | return array( |
||
| 422 | 'sigs' => $sigs, |
||
| 423 | 'sigsDocs' => $sigsDocs |
||
| 424 | ); |
||
| 425 | } |
||
| 426 | |||
| 427 | 559 | /** |
|
| 428 | * Creates a closure that will execute $callable |
||
| 429 | * |
||
| 430 | * @param $callable |
||
| 431 | * @param array $extraOptions |
||
| 432 | * @param string $plainFuncName |
||
| 433 | * @param array $funcDesc |
||
| 434 | * @return \Closure |
||
| 435 | 108 | * |
|
| 436 | 108 | * @todo validate params? In theory all validation is left to the dispatch map... |
|
| 437 | 108 | * @todo add support for $catchWarnings |
|
| 438 | 108 | */ |
|
| 439 | protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc) |
||
| 440 | { |
||
| 441 | /** |
||
| 442 | 108 | * @param Request $req |
|
| 443 | 108 | * |
|
| 444 | 108 | * @return mixed |
|
| 445 | 108 | */ |
|
| 446 | $function = function($req) use($callable, $extraOptions, $funcDesc) |
||
| 447 | { |
||
| 448 | $encoderClass = static::$namespace.'Encoder'; |
||
| 449 | $responseClass = static::$namespace.'Response'; |
||
| 450 | $valueClass = static::$namespace.'Value'; |
||
| 451 | 108 | $allowedResponseClass = static::$allowedResponseClass != '' ? static::$allowedResponseClass : $responseClass; |
|
| 452 | 108 | ||
| 453 | // validate number of parameters received |
||
| 454 | // this should be optional really, as we assume the server does the validation |
||
| 455 | $minPars = count($funcDesc['params']); |
||
| 456 | 108 | $maxPars = $minPars; |
|
| 457 | 108 | foreach ($funcDesc['params'] as $i => $param) { |
|
| 458 | 108 | if ($param['isoptional']) { |
|
| 459 | // this particular parameter is optional. We assume later ones are as well |
||
| 460 | $minPars = $i; |
||
| 461 | 108 | break; |
|
| 462 | } |
||
| 463 | 108 | } |
|
| 464 | $numPars = $req->getNumParams(); |
||
| 465 | 108 | if ($numPars < $minPars || $numPars > $maxPars) { |
|
| 466 | 108 | return new $responseClass(0, 3, 'Incorrect parameters passed to method'); |
|
| 467 | } |
||
| 468 | |||
| 469 | 108 | $encoder = new $encoderClass(); |
|
| 470 | 108 | $options = array(); |
|
| 471 | 1 | if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) { |
|
| 472 | $options[] = 'decode_php_objs'; |
||
| 473 | } |
||
| 474 | 108 | $params = $encoder->decode($req, $options); |
|
| 475 | |||
| 476 | 108 | $result = call_user_func_array($callable, $params); |
|
| 477 | |||
| 478 | if (! is_a($result, $allowedResponseClass)) { |
||
| 479 | 108 | // q: why not do the same for int, float, bool, string? |
|
| 480 | 559 | if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) { |
|
| 481 | $result = new $valueClass($result, $funcDesc['returns']); |
||
| 482 | 559 | } else { |
|
| 483 | $options = array(); |
||
| 484 | if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) { |
||
| 485 | $options[] = 'encode_php_objs'; |
||
| 486 | } |
||
| 487 | if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) { |
||
| 488 | $options[] = 'null_extension'; |
||
| 489 | } |
||
| 490 | |||
| 491 | 643 | $result = $encoder->encode($result, $options); |
|
| 492 | } |
||
| 493 | $result = new $responseClass($result); |
||
| 494 | } |
||
| 495 | 643 | ||
| 496 | return $result; |
||
| 497 | 643 | }; |
|
| 498 | 622 | ||
| 499 | 559 | return $function; |
|
| 500 | 559 | } |
|
| 501 | |||
| 502 | 559 | /** |
|
| 503 | * Return a name for a new function, based on $callable, insuring its uniqueness |
||
| 504 | * @param mixed $callable a php callable, or the name of an xml-rpc method |
||
| 505 | 622 | * @param string $newFuncName when not empty, it is used instead of the calculated version |
|
| 506 | * @return string |
||
| 507 | */ |
||
| 508 | 622 | protected function newFunctionName($callable, $newFuncName, $extraOptions) |
|
| 509 | 622 | { |
|
| 510 | 622 | // determine name of new php function |
|
| 511 | |||
| 512 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix; |
||
| 513 | |||
| 514 | 22 | if ($newFuncName == '') { |
|
| 515 | if (is_array($callable)) { |
||
| 516 | if (is_string($callable[0])) { |
||
| 517 | 643 | $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable); |
|
| 518 | 620 | } else { |
|
| 519 | $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1]; |
||
| 520 | } |
||
| 521 | 643 | } else { |
|
| 522 | if ($callable instanceof \Closure) { |
||
| 523 | $xmlrpcFuncName = "{$prefix}_closure"; |
||
| 524 | } else { |
||
| 525 | $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), |
||
| 526 | array('_', ''), $callable); |
||
| 527 | $xmlrpcFuncName = "{$prefix}_$callable"; |
||
| 528 | } |
||
| 529 | } |
||
| 530 | } else { |
||
| 531 | $xmlrpcFuncName = $newFuncName; |
||
| 532 | } |
||
| 533 | |||
| 534 | 559 | while (function_exists($xmlrpcFuncName)) { |
|
| 535 | $xmlrpcFuncName .= 'x'; |
||
| 536 | 559 | } |
|
| 537 | |||
| 538 | 559 | return $xmlrpcFuncName; |
|
| 539 | 559 | } |
|
| 540 | 559 | ||
| 541 | /** |
||
| 542 | 559 | * @param $callable |
|
| 543 | 559 | * @param string $newFuncName |
|
| 544 | 559 | * @param array $extraOptions |
|
| 545 | 559 | * @param string $plainFuncName |
|
| 546 | 559 | * @param array $funcDesc |
|
| 547 | * @return string |
||
| 548 | 559 | */ |
|
| 549 | protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc) |
||
| 550 | { |
||
| 551 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
| 552 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
||
| 553 | 559 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
| 554 | 559 | $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : ''; |
|
| 555 | 559 | ||
| 556 | $i = 0; |
||
| 557 | 559 | $parsVariations = array(); |
|
| 558 | $pars = array(); |
||
| 559 | $pNum = count($funcDesc['params']); |
||
| 560 | foreach ($funcDesc['params'] as $param) { |
||
| 561 | 559 | ||
| 562 | if ($param['isoptional']) { |
||
| 563 | // this particular parameter is optional. save as valid previous list of parameters |
||
| 564 | $parsVariations[] = $pars; |
||
| 565 | } |
||
| 566 | |||
| 567 | 559 | $pars[] = "\$params[$i]"; |
|
| 568 | 559 | $i++; |
|
| 569 | if ($i == $pNum) { |
||
| 570 | // last allowed parameters combination |
||
| 571 | $parsVariations[] = $pars; |
||
| 572 | } |
||
| 573 | 559 | } |
|
| 574 | 559 | ||
| 575 | if (count($parsVariations) == 0) { |
||
| 576 | 559 | // only known good synopsis = no parameters |
|
| 577 | 559 | $parsVariations[] = array(); |
|
| 578 | $minPars = 0; |
||
| 579 | $maxPars = 0; |
||
| 580 | 559 | } else { |
|
| 581 | $minPars = count($parsVariations[0]); |
||
| 582 | $maxPars = count($parsVariations[count($parsVariations)-1]); |
||
| 583 | } |
||
| 584 | |||
| 585 | 559 | // build body of new function |
|
| 586 | 559 | ||
| 587 | 559 | $innerCode = " \$paramCount = \$req->getNumParams();\n"; |
|
| 588 | 559 | $innerCode .= " if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n"; |
|
| 589 | |||
| 590 | 559 | $innerCode .= " \$encoder = new " . static::$namespace . "Encoder();\n"; |
|
| 591 | if ($decodePhpObjects) { |
||
| 592 | 559 | $innerCode .= " \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n"; |
|
| 593 | 559 | } else { |
|
| 594 | 559 | $innerCode .= " \$params = \$encoder->decode(\$req);\n"; |
|
| 595 | } |
||
| 596 | |||
| 597 | 559 | // since we are building source code for later use, if we are given an object instance, |
|
| 598 | 559 | // we go out of our way and store a pointer to it in a static class var. |
|
| 599 | // NB: if the code is used in a _separate_ php request, then a class to Wrapper::holdObject() will be necessary! |
||
| 600 | if (is_array($callable) && is_object($callable[0])) { |
||
| 601 | 559 | static::holdObject($newFuncName, $callable[0]); |
|
| 602 | $class = get_class($callable[0]); |
||
| 603 | if ($class[0] !== '\\') { |
||
| 604 | 559 | $class = '\\' . $class; |
|
| 605 | } |
||
| 606 | $innerCode .= " /// @var $class \$obj\n"; |
||
| 607 | $innerCode .= " \$obj = " . static::$namespace . "Wrapper::getHeldObject('$newFuncName');\n"; |
||
| 608 | $realFuncName = '$obj->' . $callable[1]; |
||
| 609 | } else { |
||
| 610 | $realFuncName = $plainFuncName; |
||
| 611 | 559 | } |
|
| 612 | foreach ($parsVariations as $i => $pars) { |
||
| 613 | 559 | $innerCode .= " if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n"; |
|
| 614 | if ($i < (count($parsVariations) - 1)) |
||
| 615 | $innerCode .= " else\n"; |
||
| 616 | } |
||
| 617 | $allowedResponseClass = static::$allowedResponseClass != '' ? static::$allowedResponseClass : static::$namespace . 'Response'; |
||
| 618 | $innerCode .= " if (is_a(\$retVal, '" . $allowedResponseClass . "'))\n return \$retVal;\n else\n"; |
||
| 619 | /// q: why not do the same for int, float, bool, string? |
||
| 620 | if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) { |
||
| 621 | $innerCode .= " return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));"; |
||
| 622 | } else { |
||
| 623 | $encodeOptions = array(); |
||
| 624 | if ($encodeNulls) { |
||
| 625 | $encodeOptions[] = 'null_extension'; |
||
| 626 | } |
||
| 627 | if ($encodePhpObjects) { |
||
| 628 | $encodeOptions[] = 'encode_php_objs'; |
||
| 629 | 559 | } |
|
| 630 | |||
| 631 | 559 | if ($encodeOptions) { |
|
| 632 | 559 | $innerCode .= " return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" . |
|
| 633 | implode("', '", $encodeOptions) . "')));"; |
||
| 634 | 559 | } else { |
|
| 635 | 559 | $innerCode .= " return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));"; |
|
| 636 | 559 | } |
|
| 637 | 559 | } |
|
| 638 | 559 | // shall we exclude functions returning by ref? |
|
| 639 | 559 | // if ($func->returnsReference()) |
|
| 640 | 559 | // return false; |
|
| 641 | 559 | ||
| 642 | $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" . |
||
| 643 | 559 | "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}"; |
|
| 644 | |||
| 645 | 559 | return $code; |
|
| 646 | 559 | } |
|
| 647 | |||
| 648 | /** |
||
| 649 | * Given a user-defined PHP class or php object, map its methods onto a list of |
||
| 650 | * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server |
||
| 651 | * object and called from remote clients (as well as their corresponding signature info). |
||
| 652 | * |
||
| 653 | 559 | * @param string|object $className the name of the class whose methods are to be exposed as xml-rpc methods, or an object instance of that class |
|
| 654 | * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus |
||
| 655 | * - string method_type 'static', 'nonstatic', 'all' and 'auto' (default); the latter will switch between static and non-static depending on whether $className is a class name or object instance |
||
| 656 | * - string method_filter a regexp used to filter methods to wrap based on their names |
||
| 657 | * - string prefix used for the names of the xml-rpc methods created. |
||
| 658 | * - string replace_class_name use to completely replace the class name with the prefix in the generated method names. e.g. instead of \Some\Namespace\Class.method use prefixmethod |
||
| 659 | * @return array|false false on failure, or on array useable for the dispatch map |
||
| 660 | * |
||
| 661 | * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on |
||
| 662 | 559 | * each invocation, for the case where all the generated functions will be saved as methods of a class |
|
| 663 | */ |
||
| 664 | 559 | public function wrapPhpClass($className, $extraOptions = array()) |
|
| 665 | 559 | { |
|
| 666 | $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : ''; |
||
| 667 | $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto'; |
||
| 668 | 559 | ||
| 669 | 559 | $results = array(); |
|
| 670 | $mList = get_class_methods($className); |
||
| 671 | foreach ($mList as $mName) { |
||
| 672 | if ($methodFilter == '' || preg_match($methodFilter, $mName)) { |
||
| 673 | 559 | $func = new \ReflectionMethod($className, $mName); |
|
| 674 | if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) { |
||
| 675 | if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) || |
||
| 676 | (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className)))) |
||
| 677 | ) { |
||
| 678 | $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions); |
||
| 679 | |||
| 680 | if ($methodWrap) { |
||
| 681 | $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap; |
||
| 682 | } |
||
| 683 | } |
||
| 684 | } |
||
| 685 | } |
||
| 686 | } |
||
| 687 | |||
| 688 | return $results; |
||
| 689 | } |
||
| 690 | |||
| 691 | /** |
||
| 692 | * @param string|object $className |
||
| 693 | * @param string $classMethod |
||
| 694 | * @param array $extraOptions |
||
| 695 | * @return string |
||
| 696 | * |
||
| 697 | * @todo php allows many more characters in identifiers than the xml-rpc spec does. We should make sure to |
||
| 698 | * replace those (while trying to make sure we are not running in collisions) |
||
| 699 | */ |
||
| 700 | protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array()) |
||
| 701 | { |
||
| 702 | if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) { |
||
| 703 | return (isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '') . $classMethod; |
||
| 704 | } |
||
| 705 | |||
| 706 | if (is_object($className)) { |
||
| 707 | $realClassName = get_class($className); |
||
| 708 | } else { |
||
| 709 | $realClassName = $className; |
||
| 710 | } |
||
| 711 | return (isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '') . "$realClassName.$classMethod"; |
||
| 712 | } |
||
| 713 | |||
| 714 | /** |
||
| 715 | * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results |
||
| 716 | * using native php types for both arguments and results. The generated php function will return a Response |
||
| 717 | * object for failed xml-rpc calls. |
||
| 718 | * |
||
| 719 | * Known limitations: |
||
| 720 | * - server must support system.methodSignature for the target xml-rpc method |
||
| 721 | 109 | * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures |
|
| 722 | * differ only by number of params and not by type, but it would be more complication than we can spare time for) |
||
| 723 | 109 | * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to |
|
| 724 | * the php function if these are structs or arrays whose (sub)members include values of type base64 |
||
| 725 | 109 | * |
|
| 726 | * Notes: the connection properties of the given client will be copied and reused for the connection used during |
||
| 727 | 109 | * the call to the generated php function. |
|
| 728 | 109 | * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation |
|
| 729 | 24 | * and an xmlrpc-connection opened+closed. |
|
| 730 | * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful |
||
| 731 | * for debugging purposes. |
||
| 732 | 86 | * |
|
| 733 | 1 | * @param Client $client an xml-rpc client set up correctly to communicate with target server |
|
| 734 | * @param string $methodName the xml-rpc method to be mapped to a php function |
||
| 735 | * @param array $extraOptions array of options that specify conversion details. Valid options include |
||
| 736 | * - integer signum the index of the method signature to use in mapping (if |
||
| 737 | 85 | * method exposes many sigs) |
|
| 738 | * - integer timeout timeout (in secs) to be used when executing function/calling remote method |
||
| 739 | 85 | * - string protocol 'http' (default), 'http11', 'https', 'h2' or 'h2c' |
|
| 740 | * - string new_function_name the name of php function to create, when return_source is used. |
||
| 741 | 85 | * If unspecified, lib will pick an appropriate name |
|
| 742 | * - string return_source if true return php code w. function definition instead of |
||
| 743 | * the function itself (closure) |
||
| 744 | * - bool encode_nulls if true, use `<nil/>` elements instead of empty string xml-rpc |
||
| 745 | * values for php null values |
||
| 746 | * - bool encode_php_objs let php objects be sent to server using the 'improved' xml-rpc |
||
| 747 | 85 | * notation, so server can deserialize them as php objects |
|
| 748 | * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with |
||
| 749 | 85 | * trusted servers --- |
|
| 750 | * - mixed return_on_fault a php value to be returned when the xml-rpc call fails/returns |
||
| 751 | * a fault response (by default the Response object is returned |
||
| 752 | * in this case). If a string is used, '%faultCode%' and |
||
| 753 | * '%faultString%' tokens will be substituted with actual error values |
||
| 754 | * - bool throw_on_fault if true, throw an exception instead of returning a Response |
||
| 755 | * in case of errors/faults; |
||
| 756 | * if a string, do the same and assume it is the exception class to throw |
||
| 757 | * - bool debug set it to 1 or 2 to see debug results of querying server for |
||
| 758 | * method synopsis |
||
| 759 | * - int simple_client_copy set it to 1 to have a lightweight copy of the $client object |
||
| 760 | 109 | * made in the generated code (only used when return_source = true) |
|
| 761 | * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true |
||
| 762 | 109 | * |
|
| 763 | 109 | * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it' |
|
| 764 | 109 | * @todo if we can not retrieve method signature, create a php function with varargs |
|
| 765 | 109 | * @todo if caller did not specify a specific sig, shall we support all of them? |
|
| 766 | * It might be hard (hence slow) to match based on type and number of arguments... |
||
| 767 | 109 | * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster) |
|
| 768 | 109 | * @todo allow creating functions which have an extra `$debug=0` parameter |
|
| 769 | 109 | */ |
|
| 770 | 109 | public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array()) |
|
| 771 | { |
||
| 772 | 109 | $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : ''; |
|
| 773 | 109 | ||
| 774 | 109 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
|
| 775 | 109 | ||
| 776 | 109 | $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions); |
|
| 777 | 24 | if (!$mSig) { |
|
| 778 | 24 | return false; |
|
| 779 | } |
||
| 780 | |||
| 781 | 86 | if ($buildIt) { |
|
| 782 | 86 | return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig); |
|
| 783 | 85 | } else { |
|
| 784 | 85 | // if in 'offline' mode, retrieve method description too. |
|
| 785 | // in online mode, favour speed of operation |
||
| 786 | $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions); |
||
| 787 | 86 | ||
| 788 | $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions); |
||
| 789 | |||
| 790 | $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc); |
||
| 791 | |||
| 792 | 86 | $results['function'] = $newFuncName; |
|
| 793 | |||
| 794 | return $results; |
||
| 795 | } |
||
| 796 | } |
||
| 797 | |||
| 798 | /** |
||
| 799 | * Retrieves an xml-rpc method signature from a server which supports system.methodSignature |
||
| 800 | * @param Client $client |
||
| 801 | 85 | * @param string $methodName |
|
| 802 | * @param array $extraOptions |
||
| 803 | 85 | * @return false|array |
|
| 804 | 85 | */ |
|
| 805 | 85 | protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array()) |
|
| 806 | { |
||
| 807 | 85 | $reqClass = static::$namespace . 'Request'; |
|
| 808 | 85 | $valClass = static::$namespace . 'Value'; |
|
| 809 | 85 | $decoderClass = static::$namespace . 'Encoder'; |
|
| 810 | |||
| 811 | 85 | $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0; |
|
| 812 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
| 813 | 85 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
|
| 814 | 85 | $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0; |
|
| 815 | 85 | ||
| 816 | 85 | $req = new $reqClass('system.methodSignature'); |
|
| 817 | 85 | $req->addParam(new $valClass($methodName)); |
|
| 818 | 85 | $origDebug = $client->getOption(Client::OPT_DEBUG); |
|
| 819 | 85 | $client->setDebug($debug); |
|
| 820 | 85 | /// @todo move setting of timeout, protocol to outside the send() call |
|
| 821 | $response = $client->send($req, $timeout, $protocol); |
||
| 822 | $client->setDebug($origDebug); |
||
| 823 | if ($response->faultCode()) { |
||
| 824 | 85 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName); |
|
| 825 | return false; |
||
| 826 | } |
||
| 827 | |||
| 828 | $mSig = $response->value(); |
||
| 829 | /// @todo what about return xml? |
||
| 830 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
||
| 831 | $decoder = new $decoderClass(); |
||
| 832 | $mSig = $decoder->decode($mSig); |
||
| 833 | } |
||
| 834 | |||
| 835 | if (!is_array($mSig) || count($mSig) <= $sigNum) { |
||
| 836 | 1 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName); |
|
| 837 | return false; |
||
| 838 | } |
||
| 839 | 1 | ||
| 840 | return $mSig[$sigNum]; |
||
| 841 | } |
||
| 842 | 1 | ||
| 843 | 1 | /** |
|
| 844 | 1 | * @param Client $client |
|
| 845 | 1 | * @param string $methodName |
|
| 846 | 1 | * @param array $extraOptions |
|
| 847 | * @return string in case of any error, an empty string is returned, no warnings generated |
||
| 848 | */ |
||
| 849 | protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array()) |
||
| 850 | 1 | { |
|
| 851 | $reqClass = static::$namespace . 'Request'; |
||
| 852 | $valClass = static::$namespace . 'Value'; |
||
| 853 | 1 | ||
| 854 | 1 | $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0; |
|
| 855 | 1 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
|
| 856 | 1 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
|
| 857 | |||
| 858 | 1 | $mDesc = ''; |
|
| 859 | 1 | ||
| 860 | 1 | $req = new $reqClass('system.methodHelp'); |
|
| 861 | $req->addParam(new $valClass($methodName)); |
||
| 862 | $origDebug = $client->getOption(Client::OPT_DEBUG); |
||
| 863 | 1 | $client->setDebug($debug); |
|
| 864 | 1 | /// @todo move setting of timeout, protocol to outside the send() call |
|
| 865 | $response = $client->send($req, $timeout, $protocol); |
||
| 866 | $client->setDebug($origDebug); |
||
| 867 | if (!$response->faultCode()) { |
||
| 868 | $mDesc = $response->value(); |
||
| 869 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
||
| 870 | $mDesc = $mDesc->scalarVal(); |
||
| 871 | 1 | } |
|
| 872 | 1 | } |
|
| 873 | 1 | ||
| 874 | 1 | return $mDesc; |
|
| 875 | 1 | } |
|
| 876 | |||
| 877 | /** |
||
| 878 | 1 | * @param Client $client |
|
| 879 | 1 | * @param string $methodName |
|
| 880 | 1 | * @param array $extraOptions @see wrapXmlrpcMethod |
|
| 881 | * @param array $mSig |
||
| 882 | * @return \Closure |
||
| 883 | 1 | * |
|
| 884 | 1 | * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case? |
|
| 885 | */ |
||
| 886 | protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig) |
||
| 887 | { |
||
| 888 | // we clone the client, so that we can modify it a bit independently of the original |
||
| 889 | 1 | $clientClone = $this->cloneClientForClosure($client); |
|
| 890 | $function = function() use($clientClone, $methodName, $extraOptions, $mSig) |
||
| 891 | { |
||
| 892 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
| 893 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
| 894 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
||
| 895 | 1 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
| 896 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
| 897 | 1 | $throwFault = false; |
|
| 898 | 1 | $decodeFault = false; |
|
| 899 | 1 | $faultResponse = null; |
|
| 900 | if (isset($extraOptions['throw_on_fault'])) { |
||
| 901 | $throwFault = $extraOptions['throw_on_fault']; |
||
| 902 | } else if (isset($extraOptions['return_on_fault'])) { |
||
| 903 | $decodeFault = true; |
||
| 904 | $faultResponse = $extraOptions['return_on_fault']; |
||
| 905 | } |
||
| 906 | |||
| 907 | $reqClass = static::$namespace . 'Request'; |
||
| 908 | $encoderClass = static::$namespace . 'Encoder'; |
||
| 909 | $valueClass = static::$namespace . 'Value'; |
||
| 910 | |||
| 911 | 1 | $encoder = new $encoderClass(); |
|
| 912 | $encodeOptions = array(); |
||
| 913 | 1 | if ($encodePhpObjects) { |
|
| 914 | $encodeOptions[] = 'encode_php_objs'; |
||
| 915 | 1 | } |
|
| 916 | if ($encodeNulls) { |
||
| 917 | $encodeOptions[] = 'null_extension'; |
||
| 918 | } |
||
| 919 | $decodeOptions = array(); |
||
| 920 | if ($decodePhpObjects) { |
||
| 921 | $decodeOptions[] = 'decode_php_objs'; |
||
| 922 | } |
||
| 923 | |||
| 924 | /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not... |
||
| 925 | |||
| 926 | // support one extra parameter: debug |
||
| 927 | 85 | $maxArgs = count($mSig)-1; // 1st element is the return type |
|
| 928 | $currentArgs = func_get_args(); |
||
| 929 | 85 | if (func_num_args() == ($maxArgs+1)) { |
|
| 930 | 85 | $debug = array_pop($currentArgs); |
|
| 931 | 85 | $clientClone->setDebug($debug); |
|
| 932 | 85 | } |
|
| 933 | 85 | ||
| 934 | 85 | $xmlrpcArgs = array(); |
|
| 935 | 85 | foreach ($currentArgs as $i => $arg) { |
|
| 936 | if ($i == $maxArgs) { |
||
| 937 | break; |
||
| 938 | } |
||
| 939 | 85 | $pType = $mSig[$i+1]; |
|
| 940 | 85 | if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' || |
|
| 941 | $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null' |
||
| 942 | ) { |
||
| 943 | 85 | // by building directly xml-rpc values when type is known and scalar (instead of encode() calls), |
|
| 944 | // we make sure to honour the xml-rpc signature |
||
| 945 | 85 | $xmlrpcArgs[] = new $valueClass($arg, $pType); |
|
| 946 | 85 | } else { |
|
| 947 | $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions); |
||
| 948 | 64 | } |
|
| 949 | 64 | } |
|
| 950 | 64 | ||
| 951 | 64 | $req = new $reqClass($methodName, $xmlrpcArgs); |
|
| 952 | // use this to get the maximum decoding flexibility |
||
| 953 | $clientClone->setOption(Client::OPT_RETURN_TYPE, 'xmlrpcvals'); |
||
| 954 | 22 | $resp = $clientClone->send($req, $timeout, $protocol); |
|
| 955 | 22 | if ($resp->faultcode()) { |
|
| 956 | if ($throwFault) { |
||
| 957 | 85 | if (is_string($throwFault)) { |
|
| 958 | throw new $throwFault($resp->faultString(), $resp->faultCode()); |
||
| 959 | 85 | } else { |
|
| 960 | throw new \PhpXmlRpc\Exception($resp->faultString(), $resp->faultCode()); |
||
| 961 | 85 | } |
|
| 962 | } else if ($decodeFault) { |
||
| 963 | if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || |
||
| 964 | (strpos($faultResponse, '%faultString%') !== false))) { |
||
| 965 | $faultResponse = str_replace(array('%faultCode%', '%faultString%'), |
||
| 966 | array($resp->faultCode(), $resp->faultString()), $faultResponse); |
||
| 967 | 85 | } |
|
| 968 | 85 | return $faultResponse; |
|
| 969 | 85 | } else { |
|
| 970 | 85 | return $resp; |
|
| 971 | 64 | } |
|
| 972 | 64 | } else { |
|
| 973 | 64 | return $encoder->decode($resp->value(), $decodeOptions); |
|
| 974 | 64 | } |
|
| 975 | }; |
||
| 976 | |||
| 977 | 64 | return $function; |
|
| 978 | } |
||
| 979 | |||
| 980 | /** |
||
| 981 | * @internal made public just for Debugger usage |
||
| 982 | * |
||
| 983 | * @param Client $client |
||
| 984 | * @param string $methodName |
||
| 985 | 64 | * @param array $extraOptions @see wrapXmlrpcMethod |
|
| 986 | 64 | * @param string $newFuncName |
|
| 987 | * @param array $mSig |
||
| 988 | 85 | * @param string $mDesc |
|
| 989 | 64 | * @return string[] keys: source, docstring |
|
| 990 | 64 | */ |
|
| 991 | public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='') |
||
| 992 | 85 | { |
|
| 993 | 85 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
|
| 994 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
| 995 | 85 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
|
| 996 | 85 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
|
| 997 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
| 998 | $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0; |
||
| 999 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix; |
||
| 1000 | $clientReturnType = isset($extraOptions['client_return_type']) ? $extraOptions['client_return_type'] : $prefix; |
||
| 1001 | $throwFault = false; |
||
| 1002 | $decodeFault = false; |
||
| 1003 | 85 | $faultResponse = null; |
|
| 1004 | if (isset($extraOptions['throw_on_fault'])) { |
||
| 1005 | 85 | $throwFault = $extraOptions['throw_on_fault']; |
|
| 1006 | 22 | } else if (isset($extraOptions['return_on_fault'])) { |
|
| 1007 | $decodeFault = true; |
||
| 1008 | 64 | $faultResponse = $extraOptions['return_on_fault']; |
|
| 1009 | } |
||
| 1010 | |||
| 1011 | 85 | $code = "function $newFuncName("; |
|
| 1012 | if ($clientCopyMode < 2) { |
||
| 1013 | 85 | // client copy mode 0 or 1 == full / partial client copy in emitted code |
|
| 1014 | $verbatimClientCopy = !$clientCopyMode; |
||
| 1015 | $innerCode = ' ' . str_replace("\n", "\n ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $clientReturnType, static::$namespace)); |
||
| 1016 | $innerCode .= "\$client->setDebug(\$debug);\n"; |
||
| 1017 | $this_ = ''; |
||
| 1018 | } else { |
||
| 1019 | // client copy mode 2 == no client copy in emitted code |
||
| 1020 | $innerCode = ''; |
||
| 1021 | $this_ = 'this->'; |
||
| 1022 | } |
||
| 1023 | $innerCode .= " \$req = new " . static::$namespace . "Request('$methodName');\n"; |
||
| 1024 | |||
| 1025 | if ($mDesc != '') { |
||
| 1026 | // take care that PHP comment is not terminated unwillingly by method description |
||
| 1027 | /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags... |
||
| 1028 | $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n"; |
||
| 1029 | } else { |
||
| 1030 | $mDesc = "/**\n * Function $newFuncName.\n"; |
||
| 1031 | } |
||
| 1032 | |||
| 1033 | // param parsing |
||
| 1034 | 22 | $innerCode .= " \$encoder = new " . static::$namespace . "Encoder();\n"; |
|
| 1035 | $plist = array(); |
||
| 1036 | 22 | $pCount = count($mSig); |
|
| 1037 | 22 | for ($i = 1; $i < $pCount; $i++) { |
|
| 1038 | 22 | $plist[] = "\$p$i"; |
|
| 1039 | 22 | $pType = $mSig[$i]; |
|
| 1040 | 22 | if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' || |
|
| 1041 | 22 | $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null' |
|
| 1042 | 22 | ) { |
|
| 1043 | 22 | // only build directly xml-rpc values when type is known and scalar |
|
| 1044 | 22 | $innerCode .= " \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n"; |
|
| 1045 | 22 | } else { |
|
| 1046 | if ($encodePhpObjects || $encodeNulls) { |
||
| 1047 | 22 | $encOpts = array(); |
|
| 1048 | 22 | if ($encodePhpObjects) { |
|
| 1049 | $encOpts[] = 'encode_php_objs'; |
||
| 1050 | 22 | } |
|
| 1051 | 22 | if ($encodeNulls) { |
|
| 1052 | 22 | $encOpts[] = 'null_extension'; |
|
| 1053 | } |
||
| 1054 | |||
| 1055 | $innerCode .= " \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n"; |
||
| 1056 | } else { |
||
| 1057 | 22 | $innerCode .= " \$p$i = \$encoder->encode(\$p$i);\n"; |
|
| 1058 | 22 | } |
|
| 1059 | 22 | } |
|
| 1060 | 22 | $innerCode .= " \$req->addParam(\$p$i);\n"; |
|
| 1061 | $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n"; |
||
| 1062 | 22 | } |
|
| 1063 | if ($clientCopyMode < 2) { |
||
| 1064 | $plist[] = '$debug = 0'; |
||
| 1065 | $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n"; |
||
| 1066 | } |
||
| 1067 | $plist = implode(', ', $plist); |
||
| 1068 | 22 | $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]); |
|
| 1069 | if ($throwFault) { |
||
| 1070 | $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\PhpXmlRpc\\Exception'); |
||
| 1071 | 22 | } else if ($decodeFault) { |
|
| 1072 | 22 | $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)"; |
|
| 1073 | } else { |
||
| 1074 | 22 | $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)"; |
|
| 1075 | 21 | } |
|
| 1076 | $mDesc .= "\n */\n"; |
||
| 1077 | |||
| 1078 | /// @todo move setting of timeout, protocol to outside the send() call |
||
| 1079 | 22 | $innerCode .= " \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n"; |
|
| 1080 | 22 | if ($throwFault) { |
|
| 1081 | 22 | if (!is_string($throwFault)) { |
|
| 1082 | 22 | $throwFault = '\\PhpXmlRpc\\Exception'; |
|
| 1083 | } |
||
| 1084 | 22 | $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())"; |
|
| 1085 | 22 | } else if ($decodeFault) { |
|
| 1086 | 22 | if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) { |
|
| 1087 | 22 | $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')"; |
|
| 1088 | 22 | } else { |
|
| 1089 | 22 | $respCode = 'return ' . var_export($faultResponse, true); |
|
| 1090 | 22 | } |
|
| 1091 | } else { |
||
| 1092 | $respCode = 'return $res'; |
||
| 1093 | 22 | } |
|
| 1094 | 22 | if ($decodePhpObjects) { |
|
| 1095 | $innerCode .= " if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));"; |
||
| 1096 | 22 | } else { |
|
| 1097 | 22 | $innerCode .= " if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());"; |
|
| 1098 | 22 | } |
|
| 1099 | 22 | ||
| 1100 | 22 | $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n"; |
|
| 1101 | |||
| 1102 | return array('source' => $code, 'docstring' => $mDesc); |
||
| 1103 | 22 | } |
|
| 1104 | |||
| 1105 | /** |
||
| 1106 | * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote |
||
| 1107 | * server as own methods. |
||
| 1108 | * For a slimmer alternative, see the code in demo/client/proxy.php. |
||
| 1109 | 22 | * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but |
|
| 1110 | 22 | * we do not support them yet... |
|
| 1111 | 22 | * |
|
| 1112 | 22 | * @see wrapXmlrpcMethod for more details. |
|
| 1113 | 22 | * |
|
| 1114 | 22 | * @param Client $client the client obj all set to query the desired server |
|
| 1115 | * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus |
||
| 1116 | * - string method_filter regular expression |
||
| 1117 | * - string new_class_name |
||
| 1118 | * - string prefix |
||
| 1119 | * - bool simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class |
||
| 1120 | * @return string|array|false false on error, the name of the created class if all ok or an array with code, class name and comments (if the appropriate option is set in extra_options) |
||
| 1121 | * |
||
| 1122 | * @todo add support for anonymous classes in the 'buildIt' case for php > 7 |
||
| 1123 | * @todo add method setDebug() to new class, to enable/disable debugging |
||
| 1124 | * @todo optimization - move the generated Encoder instance to be a property of the created class, instead of creating |
||
| 1125 | * it on every generated method invocation |
||
| 1126 | */ |
||
| 1127 | public function wrapXmlrpcServer($client, $extraOptions = array()) |
||
| 1128 | { |
||
| 1129 | $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : ''; |
||
| 1130 | $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0; |
||
| 1131 | $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : ''; |
||
| 1132 | $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : ''; |
||
| 1133 | $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false; |
||
| 1134 | $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false; |
||
| 1135 | $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false; |
||
| 1136 | $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true; |
||
| 1137 | 85 | $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false; |
|
| 1138 | $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true; |
||
| 1139 | 85 | $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix; |
|
| 1140 | 85 | $clientReturnType = isset($extraOptions['client_return_type']) ? $extraOptions['client_return_type'] : $prefix; |
|
| 1141 | |||
| 1142 | $reqClass = static::$namespace . 'Request'; |
||
| 1143 | $decoderClass = static::$namespace . 'Encoder'; |
||
| 1144 | 85 | ||
| 1145 | 85 | // retrieve the list of methods |
|
| 1146 | $req = new $reqClass('system.listMethods'); |
||
| 1147 | /// @todo move setting of timeout, protocol to outside the send() call |
||
| 1148 | $response = $client->send($req, $timeout, $protocol); |
||
| 1149 | if ($response->faultCode()) { |
||
| 1150 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server'); |
||
| 1151 | 85 | ||
| 1152 | 85 | return false; |
|
| 1153 | 85 | } |
|
| 1154 | $mList = $response->value(); |
||
| 1155 | /// @todo what about return_type = xml? |
||
| 1156 | if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') { |
||
| 1157 | $decoder = new $decoderClass(); |
||
| 1158 | 85 | $mList = $decoder->decode($mList); |
|
| 1159 | } |
||
| 1160 | 85 | if (!is_array($mList) || !count($mList)) { |
|
| 1161 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server'); |
||
| 1162 | |||
| 1163 | return false; |
||
| 1164 | } |
||
| 1165 | |||
| 1166 | // pick a suitable name for the new function, avoiding collisions |
||
| 1167 | if ($newClassName != '') { |
||
| 1168 | $xmlrpcClassName = $newClassName; |
||
| 1169 | } else { |
||
| 1170 | /// @todo direct access to $client->server is now deprecated |
||
| 1171 | $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), array('_', ''), |
||
| 1172 | $client->server) . '_client'; |
||
| 1173 | } |
||
| 1174 | while ($buildIt && class_exists($xmlrpcClassName)) { |
||
| 1175 | $xmlrpcClassName .= 'x'; |
||
| 1176 | } |
||
| 1177 | |||
| 1178 | $source = "class $xmlrpcClassName\n{\n public \$client;\n\n"; |
||
| 1179 | $source .= " function __construct()\n {\n"; |
||
| 1180 | $source .= ' ' . str_replace("\n", "\n ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $clientReturnType, static::$namespace)); |
||
| 1181 | $source .= "\$this->client = \$client;\n }\n\n"; |
||
| 1182 | $opts = array( |
||
| 1183 | 'return_source' => true, |
||
| 1184 | 'simple_client_copy' => 2, // do not produce code to copy the client object |
||
| 1185 | 'timeout' => $timeout, |
||
| 1186 | 'protocol' => $protocol, |
||
| 1187 | 'encode_nulls' => $encodeNulls, |
||
| 1188 | 'encode_php_objs' => $encodePhpObjects, |
||
| 1189 | 'decode_php_objs' => $decodePhpObjects, |
||
| 1190 | 'throw_on_fault' => $throwOnFault, |
||
| 1191 | 'prefix' => $prefix, |
||
| 1192 | ); |
||
| 1193 | |||
| 1194 | /// @todo build phpdoc for class definition, too |
||
| 1195 | foreach ($mList as $mName) { |
||
| 1196 | if ($methodFilter == '' || preg_match($methodFilter, $mName)) { |
||
| 1197 | /// @todo this will fail if server exposes 2 methods called f.e. do.something and do_something |
||
| 1198 | $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), |
||
| 1199 | array('_', ''), $mName); |
||
| 1200 | $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts); |
||
| 1201 | if ($methodWrap) { |
||
| 1202 | if ($buildIt) { |
||
| 1203 | $source .= $methodWrap['source'] . "\n"; |
||
| 1204 | |||
| 1205 | } else { |
||
| 1206 | $source .= ' ' . str_replace("\n", "\n ", $methodWrap['docstring']); |
||
| 1207 | $source .= str_replace("\n", "\n ", $methodWrap['source']). "\n"; |
||
| 1208 | } |
||
| 1209 | |||
| 1210 | } else { |
||
| 1211 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName); |
||
| 1212 | } |
||
| 1213 | } |
||
| 1214 | } |
||
| 1215 | $source .= "}\n"; |
||
| 1216 | if ($buildIt) { |
||
| 1217 | $allOK = 0; |
||
| 1218 | eval($source . '$allOK=1;'); |
||
| 1219 | if ($allOK) { |
||
| 1220 | return $xmlrpcClassName; |
||
| 1221 | } else { |
||
| 1222 | /// @todo direct access to $client->server is now deprecated |
||
| 1223 | $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName . |
||
| 1224 | ' to wrap remote server ' . $client->server); |
||
| 1225 | return false; |
||
| 1226 | } |
||
| 1227 | } else { |
||
| 1228 | return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => ''); |
||
| 1229 | } |
||
| 1230 | } |
||
| 1231 | |||
| 1232 | /** |
||
| 1233 | * Given necessary info, generate php code that will build a client object just like the given one. |
||
| 1234 | * Take care that no full checking of input parameters is done to ensure that valid php code is emitted. |
||
| 1235 | * @param Client $client |
||
| 1236 | * @param bool $verbatimClientCopy when true, copy the whole options of the client, except for 'debug' and 'return_type' |
||
| 1237 | * @param string $prefix used for the return_type of the created client (postfixed with 'vals') |
||
| 1238 | * @param string $namespace |
||
| 1239 | * @return string |
||
| 1240 | */ |
||
| 1241 | protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\') |
||
| 1242 | { |
||
| 1243 | $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->getUrl()) . |
||
| 1244 | "');\n"; |
||
| 1245 | |||
| 1246 | // copy all client fields to the client that will be generated runtime |
||
| 1247 | // (this provides for future expansion or subclassing of client obj) |
||
| 1248 | if ($verbatimClientCopy) { |
||
| 1249 | foreach ($client->getOptions() as $opt => $val) { |
||
| 1250 | if ($opt != 'debug' && $opt != 'return_type') { |
||
| 1251 | $val = var_export($val, true); |
||
| 1252 | $code .= "\$client->setOption('$opt', $val);\n"; |
||
| 1253 | } |
||
| 1254 | } |
||
| 1255 | } |
||
| 1256 | // only make sure that client always returns the correct data type |
||
| 1257 | $code .= "\$client->setOption(\PhpXmlRpc\Client::OPT_RETURN_TYPE, '{$prefix}vals');\n"; |
||
| 1258 | return $code; |
||
| 1259 | } |
||
| 1260 | |||
| 1261 | /** |
||
| 1262 | * @param string $index use '*' as wildcard for an objet to be used whenever one with the appropriate index is not found |
||
| 1263 | * @param object $object |
||
| 1264 | * @return void |
||
| 1265 | */ |
||
| 1266 | public static function holdObject($index, $object) |
||
| 1267 | { |
||
| 1268 | self::$objHolder[$index] = $object; |
||
| 1269 | } |
||
| 1270 | |||
| 1271 | /** |
||
| 1272 | * @param string $index |
||
| 1273 | * @return object |
||
| 1274 | * @throws ValueErrorException |
||
| 1275 | */ |
||
| 1276 | public static function getHeldObject($index) |
||
| 1277 | { |
||
| 1278 | if (isset(self::$objHolder[$index])) { |
||
| 1279 | return self::$objHolder[$index]; |
||
| 1280 | } |
||
| 1281 | if (isset(self::$objHolder['*'])) { |
||
| 1282 | return self::$objHolder['*']; |
||
| 1283 | } |
||
| 1284 | |||
| 1285 | throw new ValueErrorException("No object held for index '$index'"); |
||
| 1286 | } |
||
| 1287 | |||
| 1288 | /** |
||
| 1289 | * @param Client $client |
||
| 1290 | * @return Client |
||
| 1291 | */ |
||
| 1292 | protected function cloneClientForClosure($client) |
||
| 1293 | { |
||
| 1294 | return clone $client; |
||
| 1295 | } |
||
| 1296 | } |
||
| 1297 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: