Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

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