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