3 * @author Gaetano Giunta
4 * @copyright (C) 2006-2015 G. Giunta
5 * @license code licensed under the BSD License: see file license.txt
11 * PHP-XMLRPC "wrapper" class.
12 * Generate stubs to transparently access xmlrpc methods as php functions and vice-versa.
13 * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies.
15 * @todo use some better templating system for code generation?
16 * @todo implement method wrapping with preservation of php objs in calls
17 * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
21 /// used to hold a reference to object instances whose methods get wrapped by wrap_php_function(), in 'create source' mode
22 public static $objHolder = array();
25 * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
26 * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
28 * - for php 'resource' types returns empty string, since resources cannot be serialized;
29 * - for php class names returns 'struct', since php objects can be serialized as xmlrpc structs
30 * - for php arrays always return array, even though arrays sometimes serialize as json structs
31 * - for 'void' and 'null' returns 'undefined'
33 * @param string $phpType
37 public function php_2_xmlrpc_type($phpType)
39 switch (strtolower($phpType)) {
41 return Value::$xmlrpcString;
43 case Value::$xmlrpcInt: // 'int'
44 case Value::$xmlrpcI4:
45 return Value::$xmlrpcInt;
46 case Value::$xmlrpcDouble: // 'double'
47 return Value::$xmlrpcDouble;
49 case Value::$xmlrpcBoolean: // 'boolean'
52 return Value::$xmlrpcBoolean;
53 case Value::$xmlrpcArray: // 'array':
54 return Value::$xmlrpcArray;
56 case Value::$xmlrpcStruct: // 'struct'
57 return Value::$xmlrpcStruct;
58 case Value::$xmlrpcBase64:
59 return Value::$xmlrpcBase64;
63 if (class_exists($phpType)) {
64 return Value::$xmlrpcStruct;
66 // unknown: might be any 'extended' xmlrpc type
67 return Value::$xmlrpcValue;
73 * Given a string defining a phpxmlrpc type return the corresponding php type.
75 * @param string $xmlrpcType
79 public function xmlrpc_2_php_type($xmlrpcType)
81 switch (strtolower($xmlrpcType)) {
83 case 'datetime.iso8601':
85 return Value::$xmlrpcString;
99 // unknown: might be any xmlrpc type
100 return strtolower($xmlrpcType);
105 * Given a user-defined PHP function, create a PHP 'wrapper' function that can
106 * be exposed as xmlrpc method from an xmlrpc server object and called from remote
107 * clients (as well as its corresponding signature info).
109 * Since php is a typeless language, to infer types of input and output parameters,
110 * it relies on parsing the javadoc-style comment block associated with the given
111 * function. Usage of xmlrpc native types (such as datetime.dateTime.iso8601 and base64)
112 * in the @param tag is also allowed, if you need the php function to receive/send
113 * data in that particular format (note that base64 encoding/decoding is transparently
114 * carried out by the lib, while datetime vals are passed around as strings)
117 * - only works for user-defined functions, not for PHP internal functions
118 * (reflection does not support retrieving number/type of params for those)
119 * - functions returning php objects will generate special structs in xmlrpc responses:
120 * when the xmlrpc decoding of those responses is carried out by this same lib, using
121 * the appropriate param in php_xmlrpc_decode, the php objects will be rebuilt.
122 * In short: php objects can be serialized, too (except for their resource members),
123 * using this function.
124 * Other libs might choke on the very same xml that will be generated in this case
125 * (i.e. it has a nonstandard attribute on struct element tags)
127 * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard'
128 * php functions (ie. functions not expecting a single Request obj as parameter)
129 * is by making use of the functions_parameters_type class member.
131 * @param string|array $callable the name of the PHP user function to be exposed as xmlrpc method; array($obj, 'methodname') and array('class', 'methodname') are ok too
132 * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true
133 * @param array $extraOptions (optional) array of options for conversion. valid values include:
134 * - bool return_source when true, php code w. function definition will be returned, instead of a closure
135 * - bool encode_php_objs let php objects be sent to server using the 'improved' xmlrpc notation, so server can deserialize them as php objects
136 * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with trusted servers ---
137 * - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked
139 * @return array|false false on error, or an array containing the name of the new php function,
140 * its signature and docs, to be used in the server dispatch map
142 * @todo decide how to deal with params passed by ref in function definition: bomb out or allow?
143 * @todo finish using phpdoc info to build method sig if all params are named but out of order
144 * @todo add a check for params of 'resource' type
145 * @todo add some trigger_errors / error_log when returning false?
146 * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
147 * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
148 * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
150 public function wrap_php_function($callable, $newFuncName = '', $extraOptions = array())
152 $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
154 if (is_string($callable) && strpos($callable, '::') !== false) {
155 $callable = explode('::', $callable);
157 if (is_array($callable)) {
158 if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) {
159 error_log('XML-RPC: syntax for function to be wrapped is wrong');
162 if (is_string($callable[0])) {
163 $plainFuncName = implode('::', $callable);
164 } elseif (is_object($callable[0])) {
165 $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
167 $exists = method_exists($callable[0], $callable[1]);
168 } else if ($callable instanceof \Closure) {
169 $plainFuncName = 'Closure';
173 $plainFuncName = $callable;
174 $exists = function_exists($callable);
178 error_log('XML-RPC: function to be wrapped is not defined: ' . $plainFuncName);
182 $funcDesc = $this->introspectFunction($callable, $plainFuncName);
187 $funcSigs = $this->buildMethodSignatures($funcDesc);
190 $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, null, null);
192 $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions);
193 $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc);
196 /// @todo examine if $paramDocs matches $parsVariations and build array for
197 /// usage as method signature, plus put together a nice string for docs
200 'function' => $callable,
201 'signature' => $funcSigs['sigs'],
202 'docstring' => $funcDesc['desc'],
203 'signature_docs' => $funcSigs['sigsDocs'],
206 $ret['function'] = $newFuncName;
207 $ret['source'] = $code;
213 * Introspect a php callable and its phpdoc block and extract information about its signature
215 * @param callable $callable
216 * @param string $plainFuncName
217 * @return array|false
219 protected function introspectFunction($callable, $plainFuncName)
221 // start to introspect PHP code
222 if (is_array($callable)) {
223 $func = new \ReflectionMethod($callable[0], $callable[1]);
224 if ($func->isPrivate()) {
225 error_log('XML-RPC: method to be wrapped is private: ' . $plainFuncName);
228 if ($func->isProtected()) {
229 error_log('XML-RPC: method to be wrapped is protected: ' . $plainFuncName);
232 if ($func->isConstructor()) {
233 error_log('XML-RPC: method to be wrapped is the constructor: ' . $plainFuncName);
236 if ($func->isDestructor()) {
237 error_log('XML-RPC: method to be wrapped is the destructor: ' . $plainFuncName);
240 if ($func->isAbstract()) {
241 error_log('XML-RPC: method to be wrapped is abstract: ' . $plainFuncName);
244 /// @todo add more checks for static vs. nonstatic?
246 $func = new \ReflectionFunction($callable);
248 if ($func->isInternal()) {
249 // Note: from PHP 5.1.0 onward, we will possibly be able to use invokeargs
250 // instead of getparameters to fully reflect internal php functions ?
251 error_log('XML-RPC: function to be wrapped is internal: ' . $plainFuncName);
255 // retrieve parameter names, types and description from javadoc comments
257 // function description
259 // type of return val: by default 'any'
260 $returns = Value::$xmlrpcValue;
261 // desc of return val
263 // type + name of function parameters
264 $paramDocs = array();
266 $docs = $func->getDocComment();
268 $docs = explode("\n", $docs);
270 foreach ($docs as $doc) {
271 $doc = trim($doc, " \r\t/*");
272 if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
277 } elseif (strpos($doc, '@param') === 0) {
278 // syntax: @param type $name [desc]
279 if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s+(.+)?/', $doc, $matches)) {
280 $name = strtolower(trim($matches[2]));
281 //$paramDocs[$name]['name'] = trim($matches[2]);
282 $paramDocs[$name]['doc'] = $matches[3];
283 $paramDocs[$name]['type'] = $matches[1];
286 } elseif (strpos($doc, '@return') === 0) {
287 // syntax: @return type [desc]
288 if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
289 $returns = $matches[1];
290 if (isset($matches[2])) {
291 $returnsDocs = trim($matches[2]);
298 // execute introspection of actual function prototype
301 foreach ($func->getParameters() as $paramObj) {
302 $params[$i] = array();
303 $params[$i]['name'] = '$' . $paramObj->getName();
304 $params[$i]['isoptional'] = $paramObj->isOptional();
311 'params' => $params, // array, positionally indexed
312 'paramDocs' => $paramDocs, // array, indexed by name
313 'returns' => $returns,
314 'returnsDocs' =>$returnsDocs,
319 * Given the method description given by introspection, create method signature data
321 * @todo support better docs with multiple types separated by pipes by creating multiple signatures
322 * (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences)
324 * @param array $funcDesc as generated by self::introspectFunction()
328 protected function buildMethodSignatures($funcDesc)
331 $parsVariations = array();
333 $pNum = count($funcDesc['params']);
334 foreach ($funcDesc['params'] as $param) {
335 /* // match by name real param and documented params
336 $name = strtolower($param['name']);
337 if (!isset($funcDesc['paramDocs'][$name])) {
338 $funcDesc['paramDocs'][$name] = array();
340 if (!isset($funcDesc['paramDocs'][$name]['type'])) {
341 $funcDesc['paramDocs'][$name]['type'] = 'mixed';
344 if ($param['isoptional']) {
345 // this particular parameter is optional. save as valid previous list of parameters
346 $parsVariations[] = $pars;
352 // last allowed parameters combination
353 $parsVariations[] = $pars;
357 if (count($parsVariations) == 0) {
358 // only known good synopsis = no parameters
359 $parsVariations[] = array();
364 foreach ($parsVariations as $pars) {
366 $sig = array($this->php_2_xmlrpc_type($funcDesc['returns']));
367 $pSig = array($funcDesc['returnsDocs']);
368 for ($i = 0; $i < count($pars); $i++) {
369 $name = strtolower($funcDesc['params'][$i]['name']);
370 if (isset($funcDesc['paramDocs'][$name]['type'])) {
371 $sig[] = $this->php_2_xmlrpc_type($funcDesc['paramDocs'][$name]['type']);
373 $sig[] = Value::$xmlrpcValue;
375 $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : '';
383 'sigsDocs' => $sigsDocs
388 * Creates a closure that will execute $callable
389 * @todo validate params? In theory all validation is left to the dispatch map...
390 * @todo add support for $catchWarnings
393 * @param array $extraOptions
394 * @param string $plainFuncName
395 * @param string $funcDesc
398 protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
400 $function = function($req) use($callable, $extraOptions, $funcDesc)
402 $nameSpace = '\\PhpXmlRpc\\';
403 $encoderClass = $nameSpace.'Encoder';
404 $responseClass = $nameSpace.'Response';
405 $valueClass = $nameSpace.'Value';
407 $encoder = new $encoderClass();
409 if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) {
410 $options[] = 'decode_php_objs';
412 $params = $encoder->decode($req, $options);
414 $result = call_user_func_array($callable, $params);
416 if (! is_a($result, $responseClass)) {
417 if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
418 $result = new $valueClass($result, $funcDesc['returns']);
421 if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) {
422 $options[] = 'encode_php_objs';
425 $result = $encoder->encode($result, $options);
427 $result = new $responseClass($result);
437 * Return a name for a new function, based on $callable, insuring its uniqueness
438 * @param mixed $callable a php callable, or the name of an xmlrpc method
439 * @param string $newFuncName when not empty, it is used instead of the calculated version
442 protected function newFunctionName($callable, $newFuncName, $extraOptions)
444 // determine name of new php function
446 $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
448 if ($newFuncName == '') {
449 if (is_array($callable)) {
450 if (is_string($callable[0])) {
451 $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable);
453 $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1];
456 if ($callable instanceof \Closure) {
457 $xmlrpcFuncName = "{$prefix}_closure";
459 $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
460 array('_', ''), $callable);
461 $xmlrpcFuncName = "{$prefix}_$callable";
465 $xmlrpcFuncName = $newFuncName;
468 while (function_exists($xmlrpcFuncName)) {
469 $xmlrpcFuncName .= 'x';
472 return $xmlrpcFuncName;
477 * @param string $newFuncName
478 * @param array $extraOptions
479 * @param string $plainFuncName
480 * @param array $funcDesc
483 protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
485 $namespace = '\\PhpXmlRpc\\';
487 $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
488 $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
489 $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : '';
491 // build body of new function
493 $innerCode = "\$encoder = new {$namespace}Encoder();\n";
495 $parsVariations = array();
497 $pNum = count($funcDesc['params']);
498 foreach ($funcDesc['params'] as $param) {
499 /*$name = strtolower($funcDesc['params'][$i]['name']);
500 if (!isset($funcDesc['paramDocs'][$name])) {
501 // no param found in phpdoc info matching param definition!
502 $funcDesc['paramDocs'][$name]['type'] = 'mixed';
505 if ($param['isoptional']) {
506 // this particular parameter is optional. save as valid previous list of parameters
507 $innerCode .= "if (\$paramcount > $i) {\n";
508 $parsVariations[] = $pars;
510 $innerCode .= "\$p$i = \$req->getParam($i);\n";
511 if ($decodePhpObjects) {
512 $innerCode .= "if (\$p{$i}->kindOf() == 'scalar') \$p$i = \$p{$i}->scalarval(); else \$p$i = \$encoder->decode(\$p$i, array('decode_php_objs'));\n";
514 $innerCode .= "if (\$p{$i}->kindOf() == 'scalar') \$p$i = \$p{$i}->scalarval(); else \$p$i = \$encoder->decode(\$p$i);\n";
519 if ($param['isoptional']) {
523 // last allowed parameters combination
524 $parsVariations[] = $pars;
528 if (count($parsVariations) == 0) {
529 // only known good synopsis = no parameters
530 $parsVariations[] = array();
533 $minPars = count($parsVariations[0]);
537 // add to code the check for min params number
538 // NB: this check needs to be done BEFORE decoding param values
539 $innerCode = "\$paramcount = \$req->getNumParams();\n" .
540 "if (\$paramcount < $minPars) return new {$namespace}Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n" . $innerCode;
542 $innerCode = "\$paramcount = \$req->getNumParams();\n" . $innerCode;
545 $innerCode .= "\$np = false;\n";
546 // since we are building source code for later use, if we are given an object instance,
547 // we go out of our way and store a pointer to it in a static class var var...
548 if (is_array($callable) && is_object($callable[0])) {
549 self::$objHolder[$newFuncName] = $callable[0];
550 $innerCode .= "\$obj = PhpXmlRpc\\Wrapper::\$objHolder['$newFuncName'];\n";
551 $realFuncName = '$obj->' . $callable[1];
553 $realFuncName = $plainFuncName;
555 foreach ($parsVariations as $pars) {
556 $innerCode .= "if (\$paramcount == " . count($pars) . ") \$retval = {$catchWarnings}$realFuncName(" . implode(',', $pars) . "); else\n";
558 $innerCode .= "\$np = true;\n";
559 $innerCode .= "if (\$np) return new {$namespace}Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "'); else {\n";
560 //$innerCode .= "if (\$_xmlrpcs_error_occurred) return new Response(0, $GLOBALS['xmlrpcerr']user, \$_xmlrpcs_error_occurred); else\n";
561 $innerCode .= "if (is_a(\$retval, '{$namespace}Response')) return \$retval; else\n";
562 if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
563 $innerCode .= "return new {$namespace}Response(new {$namespace}Value(\$retval, '{$funcDesc['returns']}'));";
565 if ($encodePhpObjects) {
566 $innerCode .= "return new {$namespace}Response(\$encoder->encode(\$retval, array('encode_php_objs')));\n";
568 $innerCode .= "return new {$namespace}Response(\$encoder->encode(\$retval));\n";
571 // shall we exclude functions returning by ref?
572 // if($func->returnsReference())
575 $code = "function $newFuncName(\$req) {\n" . $innerCode . "}\n}";
581 * Given a user-defined PHP class or php object, map its methods onto a list of
582 * PHP 'wrapper' functions that can be exposed as xmlrpc methods from an xmlrpc server
583 * object and called from remote clients (as well as their corresponding signature info).
585 * @param mixed $className the name of the class whose methods are to be exposed as xmlrpc methods, or an object instance of that class
586 * @param array $extraOptions see the docs for wrap_php_method for basic options, plus
587 * - 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
588 * - string method_filter a regexp used to filter methods to wrap based on their names
589 * - string prefix used for the names of the xmlrpc methods created
591 * @return array|false false on failure
593 public function wrap_php_class($className, $extraOptions = array())
595 $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
596 $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
597 $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '';
600 $mList = get_class_methods($className);
601 foreach ($mList as $mName) {
602 if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
603 $func = new \ReflectionMethod($className, $mName);
604 if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
605 if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) ||
606 (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
608 $methodWrap = $this->wrap_php_function(array($className, $mName), '', $extraOptions);
610 if (is_object($className)) {
611 $realClassName = get_class($className);
613 $realClassName = $className;
615 $results[$prefix."$realClassName.$mName"] = $methodWrap;
626 * Given an xmlrpc client and a method name, register a php wrapper function
627 * that will call it and return results using native php types for both
628 * params and results. The generated php function will return a Response
629 * object for failed xmlrpc calls.
632 * - server must support system.methodsignature for the wanted xmlrpc method
633 * - for methods that expose many signatures, only one can be picked (we
634 * could in principle check if signatures differ only by number of params
635 * and not by type, but it would be more complication than we can spare time)
636 * - nested xmlrpc params: the caller of the generated php function has to
637 * encode on its own the params passed to the php function if these are structs
638 * or arrays whose (sub)members include values of type datetime or base64
640 * Notes: the connection properties of the given client will be copied
641 * and reused for the connection used during the call to the generated
643 * Calling the generated php function 'might' be slow: a new xmlrpc client
644 * is created on every invocation and an xmlrpc-connection opened+closed.
645 * An extra 'debug' param is appended to param list of xmlrpc method, useful
646 * for debugging purposes.
648 * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it'
649 * @todo if we can not retrieve method signature, create a php function with varargs
650 * @todo allow the created function to throw exceptions on method calls failures
651 * @todo if caller did not specify a specific sig, shall we support all of them?
652 * It might be hard (hence slow) to match based on type and number of arguments...
654 * @param Client $client an xmlrpc client set up correctly to communicate with target server
655 * @param string $methodName the xmlrpc method to be mapped to a php function
656 * @param array $extraOptions array of options that specify conversion details. Valid options include
657 * - integer signum the index of the method signature to use in mapping (if method exposes many sigs)
658 * - integer timeout timeout (in secs) to be used when executing function/calling remote method
659 * - string protocol 'http' (default), 'http11' or 'https'
660 * - string new_function_name the name of php function to create, when return_source is used. If unspecified, lib will pick an appropriate name
661 * - string return_source if true return php code w. function definition instead of function itself (closure)
662 * - bool encode_php_objs let php objects be sent to server using the 'improved' xmlrpc notation, so server can deserialize them as php objects
663 * - bool decode_php_objs --- WARNING !!! possible security hazard. only use it with trusted servers ---
664 * - 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
665 * - bool debug set it to 1 or 2 to see debug results of querying server for method synopsis
666 * - 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)
668 * @return \closure|array|false false on failure, closure by default and array for return_source = true
670 public function wrap_xmlrpc_method($client, $methodName, $extraOptions = array())
672 $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : '';
674 $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
676 $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions);
682 return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig);
684 // if in 'offline' mode, retrieve method description too.
685 // in online mode, favour speed of operation
686 $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions);
688 $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions);
690 $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc);
691 /* was: $results = $this->build_remote_method_wrapper_code($client, $methodName,
692 $newFuncName, $mSig, $mDesc, $timeout, $protocol, $simpleClientCopy,
693 $prefix, $decodePhpObjects, $encodePhpObjects, $decodeFault,
694 $faultResponse, $namespace);*/
696 $results['function'] = $newFuncName;
704 * Retrieves an xmlrpc method signature from a server which supports system.methodSignature
705 * @param Client $client
706 * @param string $methodName
707 * @param array $extraOptions
708 * @return false|array
710 protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array())
712 $namespace = '\\PhpXmlRpc\\';
713 $reqClass = $namespace . 'Request';
714 $valClass = $namespace . 'Value';
715 $decoderClass = $namespace . 'Encoder';
717 $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
718 $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
719 $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
720 $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0;
722 $req = new $reqClass('system.methodSignature');
723 $req->addparam(new $valClass($methodName));
724 $client->setDebug($debug);
725 $response = $client->send($req, $timeout, $protocol);
726 if ($response->faultCode()) {
727 error_log('XML-RPC: could not retrieve method signature from remote server for method ' . $methodName);
731 $mSig = $response->value();
732 if ($client->return_type != 'phpvals') {
733 $decoder = new $decoderClass();
734 $mSig = $decoder->decode($mSig);
737 if (!is_array($mSig) || count($mSig) <= $sigNum) {
738 error_log('XML-RPC: could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName);
742 return $mSig[$sigNum];
746 * @param Client $client
747 * @param string $methodName
748 * @param array $extraOptions
749 * @return string in case of any error, an empty string is returned, no warnings generated
751 protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array())
753 $namespace = '\\PhpXmlRpc\\';
754 $reqClass = $namespace . 'Request';
755 $valClass = $namespace . 'Value';
757 $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
758 $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
759 $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
763 $req = new $reqClass('system.methodHelp');
764 $req->addparam(new $valClass($methodName));
765 $client->setDebug($debug);
766 $response = $client->send($req, $timeout, $protocol);
767 if (!$response->faultCode()) {
768 $mDesc = $response->value();
769 if ($client->return_type != 'phpvals') {
770 $mDesc = $mDesc->scalarval();
778 * @param Client $client
779 * @param string $methodName
780 * @param array $extraOptions
781 * @param string $mSig
784 * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case?
786 protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig)
788 // we clone the client, so that we can modify it a bit independently of the original
789 $clientClone = clone $client;
790 $function = function() use($clientClone, $methodName, $extraOptions, $mSig)
792 $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
793 $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
794 $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
795 $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
796 if (isset($extraOptions['return_on_fault'])) {
798 $faultResponse = $extraOptions['return_on_fault'];
800 $decodeFault = false;
803 $namespace = '\\PhpXmlRpc\\';
804 $reqClass = $namespace . 'Request';
805 $encoderClass = $namespace . 'Encoder';
806 $valueClass = $namespace . 'Value';
808 $encoder = new $encoderClass();
809 $encodeOptions = array();
810 if ($encodePhpObjects) {
811 $encodeOptions[] = 'encode_php_objs';
813 $decodeOptions = array();
814 if ($decodePhpObjects) {
815 $decodeOptions[] = 'decode_php_objs';
818 /// @todo check for insufficient nr. of args besides excess ones
820 // support one extra parameter: debug
821 $maxArgs = count($mSig)-1; // 1st element is the return type
822 $currentArgs = func_get_args();
823 if (func_num_args() == ($maxArgs+1)) {
824 $debug = array_pop($currentArgs);
825 $clientClone->setDebug($debug);
828 $xmlrpcArgs = array();
829 foreach($currentArgs as $i => $arg) {
830 if ($i == $maxArgs) {
831 /// @todo log warning? check what happens with the 'source' version
834 $pType = $mSig[$i+1];
835 if ($pType == 'i4' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
836 $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
838 // by building directly xmlrpc values when type is known and scalar (instead of encode() calls),
839 // we make sure to honour the xmlrpc signature
840 $xmlrpcArgs[] = new $valueClass($arg, $pType);
842 $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions);
846 $req = new $reqClass($methodName, $xmlrpcArgs);
847 // use this to get the maximum decoding flexibility
848 $clientClone->return_type = 'xmlrpcvals';
849 $resp = $clientClone->send($req, $timeout, $protocol);
850 if ($resp->faultcode()) {
852 if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) ||
853 (strpos($faultResponse, '%faultString%') !== false))) {
854 $faultResponse = str_replace(array('%faultCode%', '%faultString%'),
855 array($resp->faultCode(), $resp->faultString()), $faultResponse);
857 return $faultResponse;
862 return $encoder->decode($resp->value(), $decodeOptions);
869 protected function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='')
871 $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
872 $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
873 $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
874 $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
875 $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
876 $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
877 if (isset($extraOptions['return_on_fault'])) {
879 $faultResponse = $extraOptions['return_on_fault'];
881 $decodeFault = false;
885 $namespace = '\\PhpXmlRpc\\';
887 $code = "function $newFuncName (";
888 if ($clientCopyMode < 2) {
889 // client copy mode 0 or 1 == full / partial client copy in emitted code
890 $verbatimClientCopy = !$clientCopyMode;
891 $innerCode = $this->build_client_wrapper_code($client, $verbatimClientCopy, $prefix, $namespace);
892 $innerCode .= "\$client->setDebug(\$debug);\n";
895 // client copy mode 2 == no client copy in emitted code
899 $innerCode .= "\$req = new {$namespace}Request('$methodName');\n";
902 // take care that PHP comment is not terminated unwillingly by method description
903 $mDesc = "/**\n* " . str_replace('*/', '* /', $mDesc) . "\n";
905 $mDesc = "/**\nFunction $newFuncName\n";
909 $innerCode .= "\$encoder = new {$namespace}Encoder();\n";
911 $pCount = count($mSig);
912 for ($i = 1; $i < $pCount; $i++) {
915 if ($pType == 'i4' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
916 $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
918 // only build directly xmlrpc values when type is known and scalar
919 $innerCode .= "\$p$i = new {$namespace}Value(\$p$i, '$pType');\n";
921 if ($encodePhpObjects) {
922 $innerCode .= "\$p$i = \$encoder->encode(\$p$i, array('encode_php_objs'));\n";
924 $innerCode .= "\$p$i = \$encoder->encode(\$p$i);\n";
927 $innerCode .= "\$req->addparam(\$p$i);\n";
928 $mDesc .= '* @param ' . $this->xmlrpc_2_php_type($pType) . " \$p$i\n";
930 if ($clientCopyMode < 2) {
931 $plist[] = '$debug=0';
932 $mDesc .= "* @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n";
934 $plist = implode(', ', $plist);
935 $mDesc .= '* @return ' . $this->xmlrpc_2_php_type($mSig[0]) . " (or an {$namespace}Response obj instance if call fails)\n*/\n";
937 $innerCode .= "\$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
939 if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
940 $respCode = "str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
942 $respCode = var_export($faultResponse, true);
947 if ($decodePhpObjects) {
948 $innerCode .= "if (\$res->faultcode()) return $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
950 $innerCode .= "if (\$res->faultcode()) return $respCode; else return \$encoder->decode(\$res->value());";
953 $code = $code . $plist . ") {\n" . $innerCode . "\n}\n";
955 return array('source' => $code, 'docstring' => $mDesc);
959 * Similar to wrap_xmlrpc_method, but will generate a php class that wraps
960 * all xmlrpc methods exposed by the remote server as own methods.
961 * For more details see wrap_xmlrpc_method.
963 * For a slimmer alternative, see the code in demo/client/proxy.php
965 * Note that unlike wrap_xmlrpc_method, we always have to generate php code here. It seems that php 7 will have anon classes...
967 * @param Client $client the client obj all set to query the desired server
968 * @param array $extraOptions list of options for wrapped code. See the ones from wrap_xmlrpc_method plus
969 * - string method_filter regular expression
970 * - string new_class_name
972 * - bool simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class
974 * @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 appropriatevoption is set in extra_options)
976 public function wrap_xmlrpc_server($client, $extraOptions = array())
978 $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
979 $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
980 $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
981 $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : '';
982 $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
983 $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
984 $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true;
985 $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
986 $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
987 $namespace = '\\PhpXmlRpc\\';
989 $reqClass = $namespace . 'Request';
990 $decoderClass = $namespace . 'Encoder';
992 $req = new $reqClass('system.listMethods');
993 $response = $client->send($req, $timeout, $protocol);
994 if ($response->faultCode()) {
995 error_log('XML-RPC: could not retrieve method list from remote server');
999 $mList = $response->value();
1000 if ($client->return_type != 'phpvals') {
1001 $decoder = new $decoderClass();
1002 $mList = $decoder->decode($mList);
1004 if (!is_array($mList) || !count($mList)) {
1005 error_log('XML-RPC: could not retrieve meaningful method list from remote server');
1009 // pick a suitable name for the new function, avoiding collisions
1010 if ($newClassName != '') {
1011 $xmlrpcClassName = $newClassName;
1013 $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1014 array('_', ''), $client->server) . '_client';
1016 while ($buildIt && class_exists($xmlrpcClassName)) {
1017 $xmlrpcClassName .= 'x';
1020 /// @todo add function setdebug() to new class, to enable/disable debugging
1021 $source = "class $xmlrpcClassName\n{\npublic \$client;\n\n";
1022 $source .= "function __construct()\n{\n";
1023 $source .= $this->build_client_wrapper_code($client, $verbatimClientCopy, $prefix, $namespace);
1024 $source .= "\$this->client = \$client;\n}\n\n";
1026 'return_source' => true,
1027 'simple_client_copy' => 2, // do not produce code to copy the client object
1028 'timeout' => $timeout,
1029 'protocol' => $protocol,
1030 'encode_php_objs' => $encodePhpObjects,
1031 'decode_php_objs' => $decodePhpObjects,
1032 'prefix' => $prefix,
1034 /// @todo build phpdoc for class definition, too
1035 foreach ($mList as $mName) {
1036 if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
1037 // note: this will fail if server exposes 2 methods called f.e. do.something and do_something
1038 $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1039 array('_', ''), $mName);
1040 $methodWrap = $this->wrap_xmlrpc_method($client, $mName, $opts);
1043 $source .= $methodWrap['docstring'];
1045 $source .= $methodWrap['source'] . "\n";
1047 error_log('XML-RPC: will not create class method to wrap remote method ' . $mName);
1054 eval($source . '$allOK=1;');
1056 return $xmlrpcClassName;
1058 error_log('XML-RPC: could not create class ' . $xmlrpcClassName . ' to wrap remote server ' . $client->server);
1062 return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => '');
1069 * Given necessary info, generate php code that will build a client object just like the given one.
1070 * Take care that no full checking of input parameters is done to ensure that
1071 * valid php code is emitted.
1072 * @param Client $client
1073 * @param bool $verbatimClientCopy when true, copy all of the state of the client, except for 'debug' and 'return_type'
1074 * @param string $prefix used for the return_type of the created client
1075 * @param string $namespace
1079 protected function build_client_wrapper_code($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\' )
1081 $code = "\$client = new {$namespace}Client('" . str_replace("'", "\'", $client->path) .
1082 "', '" . str_replace("'", "\'", $client->server) . "', $client->port);\n";
1084 // copy all client fields to the client that will be generated runtime
1085 // (this provides for future expansion or subclassing of client obj)
1086 if ($verbatimClientCopy) {
1087 foreach ($client as $fld => $val) {
1088 if ($fld != 'debug' && $fld != 'return_type') {
1089 $val = var_export($val, true);
1090 $code .= "\$client->$fld = $val;\n";
1094 // only make sure that client always returns the correct data type
1095 $code .= "\$client->return_type = '{$prefix}vals';\n";
1096 //$code .= "\$client->setDebug(\$debug);\n";