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