Issues (320)

src/Wrapper.php (1 issue)

1
<?php
2
/**
3
 * @author Gaetano Giunta
4
 * @copyright (C) 2006-2025 G. Giunta
5
 * @license code licensed under the BSD License: see file license.txt
6
 */
7
8
namespace PhpXmlRpc;
9
10
use PhpXmlRpc\Exception\ValueErrorException;
11
use PhpXmlRpc\Traits\LoggerAware;
12
13
/**
14
 * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa.
15
 * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies.
16
 *
17
 * @todo use some better templating system for code generation?
18
 * @todo implement method wrapping with preservation of php objs in calls
19
 * @todo add support for 'epivals' mode
20
 * @todo allow setting custom namespace for generated wrapping code
21
 */
22
class Wrapper
23
{
24
    use LoggerAware;
25
26
    /**
27
     * @var object[]
28
     * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode
29 24
     * @internal this property will become protected in the future
30
     */
31 24
    public static $objHolder = array();
32 1
33
    /** @var string */
34 24
    protected static $namespace = '\\PhpXmlRpc\\';
35
36
    /**
37
     * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
38
     * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
39
     * Notes:
40
     * - for php 'resource' types returns empty string, since resources cannot be serialized;
41
     * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs
42
     * - for php arrays always return array, even though arrays sometimes serialize as structs...
43
     * - for 'void' and 'null' returns 'undefined'
44
     *
45
     * @param string $phpType
46
     * @return string
47
     *
48
     * @todo support notation `something[]` as 'array'
49
     * @todo check if nil support is enabled when finding null or void (which makes sense in php for return type)
50
     */
51
    public function php2XmlrpcType($phpType)
52
    {
53
        switch (strtolower($phpType)) {
54
            case 'string':
55
                return Value::$xmlrpcString;
56
            case 'integer':
57 559
            case Value::$xmlrpcInt: // 'int'
58
            case Value::$xmlrpcI4:
59 559
            case Value::$xmlrpcI8:
60 559
                return Value::$xmlrpcInt;
61 559
            case Value::$xmlrpcDouble: // 'double'
62 559
                return Value::$xmlrpcDouble;
63 559
            case 'bool':
64 559
            case Value::$xmlrpcBoolean: // 'boolean'
65 559
            case 'false':
66 559
            case 'true':
67 559
                return Value::$xmlrpcBoolean;
68
            case Value::$xmlrpcArray: // 'array':
69 559
            case 'array[]':
70 559
                return Value::$xmlrpcArray;
71 559
            case 'object':
72 559
            case Value::$xmlrpcStruct: // 'struct'
73
                return Value::$xmlrpcStruct;
74 559
            case Value::$xmlrpcBase64:
75 559
                return Value::$xmlrpcBase64;
76
            case 'resource':
77 559
                return '';
78 559
            default:
79
                if (class_exists($phpType)) {
80 559
                    // DateTimeInterface is not present in php 5.4...
81
                    if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) {
82 559
                        return Value::$xmlrpcDateTime;
83
                    }
84
                    return Value::$xmlrpcStruct;
85 559
                } else {
86 559
                    // unknown: might be any 'extended' xml-rpc type
87
                    return Value::$xmlrpcValue;
88
                }
89 559
        }
90
    }
91
92 559
    /**
93
     * Given a string defining a phpxmlrpc type return the corresponding php type.
94
     *
95
     * @param string $xmlrpcType
96
     * @return string
97
     */
98
    public function xmlrpc2PhpType($xmlrpcType)
99
    {
100
        switch (strtolower($xmlrpcType)) {
101
            case 'base64':
102
            case 'datetime.iso8601':
103
            case 'string':
104 85
                return Value::$xmlrpcString;
105
            case 'int':
106 85
            case 'i4':
107 85
            case 'i8':
108 85
                return 'integer';
109 85
            case 'struct':
110 64
            case 'array':
111 85
                return 'array';
112 22
            case 'double':
113 22
                return 'float';
114 64
            case 'undefined':
115 22
                return 'mixed';
116 22
            case 'boolean':
117
            case 'null':
118 22
            default:
119
                // unknown: might be any xml-rpc type
120 22
                return strtolower($xmlrpcType);
121 22
        }
122
    }
123
124
    /**
125
     * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an
126
     * xml-rpc server object and called from remote clients (as well as its corresponding signature info).
127
     *
128
     * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the
129
     * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as
130
     * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to
131
     * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by
132
     * the lib, while datetime values are passed around as strings)
133
     *
134
     * Known limitations:
135
     * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving
136
     *   number/type of params for those)
137
     * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of
138
     *   those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php
139
     *   objects will be rebuilt.
140
     *   In short: php objects can be serialized, too (except for their resource members), using this function.
141
     *   Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard
142
     *   attribute on struct element tags)
143
     *
144
     * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions
145
     * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and
146
     * $exception_handling properties.
147
     *
148
     * @param \Callable $callable the PHP user function to be exposed as xml-rpc method: a closure, function name, array($obj, 'methodname') or array('class', 'methodname') are ok
149
     * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true
150
     * @param array $extraOptions (optional) array of options for conversion. valid values include:
151
     *                            - bool return_source     when true, php code w. function definition will be returned, instead of a closure
152
     *                            - bool encode_nulls      let php objects be sent to server using <nil> elements instead of empty strings
153
     *                            - bool encode_php_objs   let php objects be sent to server using the 'improved' xml-rpc notation, so server can deserialize them as php objects
154
     *                            - bool decode_php_objs   --- WARNING !!! possible security hazard. only use it with trusted servers ---
155
     *                            - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked
156
     * @return array|false false on error, or an array containing the name of the new php function,
157
     *                     its signature and docs, to be used in the server dispatch map
158
     *
159
     * @todo decide how to deal with params passed by ref in function definition: bomb out or allow?
160
     * @todo finish using phpdoc info to build method sig if all params are named but out of order
161
     * @todo add a check for params of 'resource' type
162
     * @todo add some error logging when returning false?
163
     * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
164
     * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
165
     * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
166
     * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway
167
     */
168
    public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array())
169
    {
170
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
171
172
        if (is_string($callable) && strpos($callable, '::') !== false) {
173
            $callable = explode('::', $callable);
174
        }
175
        if (is_array($callable)) {
176
            if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) {
177 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong');
178
                return false;
179 559
            }
180
            if (is_string($callable[0])) {
181 559
                $plainFuncName = implode('::', $callable);
182 559
            } elseif (is_object($callable[0])) {
183
                $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
184 559
            }
185 559
            $exists = method_exists($callable[0], $callable[1]);
186
        } else if ($callable instanceof \Closure) {
187
            // we do not support creating code which wraps closures, as php does not allow to serialize them
188
            if (!$buildIt) {
189 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code');
190 559
                return false;
191 559
            }
192 559
193
            $plainFuncName = 'Closure';
194 559
            $exists = true;
195 559
        } else {
196
            $plainFuncName = $callable;
197 559
            $exists = function_exists($callable);
198
        }
199
200
        if (!$exists) {
201
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName);
202 559
            return false;
203 559
        }
204
205 559
        $funcDesc = $this->introspectFunction($callable, $plainFuncName);
206 559
        if (!$funcDesc) {
207
            return false;
208
        }
209 559
210
        $funcSigs = $this->buildMethodSignatures($funcDesc);
211
212
        if ($buildIt) {
213
            $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc);
214 559
        } else {
215 559
            $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions);
216
            $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc);
217
        }
218
219 559
        $ret = array(
220
            'function' => $callable,
221 559
            'signature' => $funcSigs['sigs'],
222 559
            'docstring' => $funcDesc['desc'],
223
            'signature_docs' => $funcSigs['sigsDocs'],
224 559
        );
225 559
        if (!$buildIt) {
226
            $ret['function'] = $newFuncName;
227
            $ret['source'] = $code;
228
        }
229 559
        return $ret;
230 559
    }
231 559
232 559
    /**
233
     * Introspect a php callable and its phpdoc block and extract information about its signature
234 559
     *
235 559
     * @param callable $callable
236 559
     * @param string $plainFuncName
237
     * @return array|false
238 559
     */
239
    protected function introspectFunction($callable, $plainFuncName)
240
    {
241
        // start to introspect PHP code
242
        if (is_array($callable)) {
243
            $func = new \ReflectionMethod($callable[0], $callable[1]);
244
            if ($func->isPrivate()) {
245
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
246
                return false;
247
            }
248 559
            if ($func->isProtected()) {
249
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
250
                return false;
251 559
            }
252 559
            if ($func->isConstructor()) {
253 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
254
                return false;
255
            }
256
            if ($func->isDestructor()) {
257 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
258
                return false;
259
            }
260
            if ($func->isAbstract()) {
261 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName);
262
                return false;
263
            }
264
            /// @todo add more checks for static vs. nonstatic?
265 559
        } else {
266
            $func = new \ReflectionFunction($callable);
267
        }
268
        if ($func->isInternal()) {
269 559
            /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully
270
            ///       reflect internal php functions
271
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName);
272
            return false;
273
        }
274
275 559
        // retrieve parameter names, types and description from javadoc comments
276
277 559
        // function description
278
        $desc = '';
279
        // type of return val: by default 'any'
280
        $returns = Value::$xmlrpcValue;
281
        // desc of return val
282
        $returnsDocs = '';
283
        // type + name of function parameters
284
        $paramDocs = array();
285
286
        $docs = $func->getDocComment();
287 559
        if ($docs != '') {
288
            $docs = explode("\n", $docs);
289 559
            $i = 0;
290
            foreach ($docs as $doc) {
291 559
                $doc = trim($doc, " \r\t/*");
292
                if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
293 559
                    if ($desc) {
294
                        $desc .= "\n";
295 559
                    }
296 559
                    $desc .= $doc;
297 559
                } elseif (strpos($doc, '@param') === 0) {
298 559
                    // syntax: @param type $name [desc]
299 559
                    if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) {
300 559
                        $name = strtolower(trim($matches[2]));
301 559
                        //$paramDocs[$name]['name'] = trim($matches[2]);
302 559
                        $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : '';
303 559
                        $paramDocs[$name]['type'] = $matches[1];
304
                    }
305 559
                    $i++;
306 559
                } elseif (strpos($doc, '@return') === 0) {
307
                    // syntax: @return type [desc]
308 559
                    if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
309 559
                        $returns = $matches[1];
310
                        if (isset($matches[2])) {
311 559
                            $returnsDocs = trim($matches[2]);
312 559
                        }
313
                    }
314 559
                }
315 559
            }
316
        }
317 559
318 559
        // for php 7+, we can take advantage of type declarations!
319 559
        if (method_exists($func, 'getReturnType')) {
320 559
            $returnType = $func->getReturnType();
321
            if ($returnType !== null) {
322
/// @todo
323
            }
324
        }
325
326
        // execute introspection of actual function prototype
327
        $params = array();
328 559
        $i = 0;
329 559
        foreach ($func->getParameters() as $paramObj) {
330 559
            $params[$i] = array();
331 559
            $params[$i]['name'] = '$' . $paramObj->getName();
332 559
            $params[$i]['isoptional'] = $paramObj->isOptional();
333 559
            if (method_exists($paramObj, 'getType')) {
334 559
                $paramType = $paramObj->getType();
335
                if ($paramType !== null) {
336
/// @todo
337
                }
338 559
            }
339 559
            $i++;
340 559
        }
341 559
342 559
        return array(
343 559
            'desc' => $desc,
344
            'docs' => $docs,
345
            'params' => $params, // array, positionally indexed
346
            'paramDocs' => $paramDocs, // array, indexed by name
347
            'returns' => $returns,
348
            'returnsDocs' =>$returnsDocs,
349
        );
350
    }
351
352
    /**
353
     * Given the method description given by introspection, create method signature data
354
     *
355
     * @param array $funcDesc as generated by self::introspectFunction()
356
     * @return array
357 559
     *
358
     * @todo support better docs with multiple types separated by pipes by creating multiple signatures
359 559
     *       (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences,
360 559
     *       but it makes a lot of sense in a php >= 8 world)
361 559
     */
362 559
    protected function buildMethodSignatures($funcDesc)
363 559
    {
364
        $i = 0;
365
        $parsVariations = array();
366
        $pars = array();
367
        $pNum = count($funcDesc['params']);
368
        foreach ($funcDesc['params'] as $param) {
369
            /* // match by name real param and documented params
370
            $name = strtolower($param['name']);
371
            if (!isset($funcDesc['paramDocs'][$name])) {
372
                $funcDesc['paramDocs'][$name] = array();
373 559
            }
374
            if (!isset($funcDesc['paramDocs'][$name]['type'])) {
375
                $funcDesc['paramDocs'][$name]['type'] = 'mixed';
376
            }*/
377
378 559
            if ($param['isoptional']) {
379 559
                // this particular parameter is optional. save as valid previous list of parameters
380 559
                $parsVariations[] = $pars;
381
            }
382 559
383
            $pars[] = "\$p$i";
384
            $i++;
385
            if ($i == $pNum) {
386 559
                // last allowed parameters combination
387
                $parsVariations[] = $pars;
388 559
            }
389
        }
390
391 559
        if (count($parsVariations) == 0) {
392 559
            // only known good synopsis = no parameters
393 559
            $parsVariations[] = array();
394
        }
395 559
396 559
        $sigs = array();
397 559
        $sigsDocs = array();
398 559
        foreach ($parsVariations as $pars) {
399 559
            // build a signature
400 559
            $sig = array($this->php2XmlrpcType($funcDesc['returns']));
401
            $pSig = array($funcDesc['returnsDocs']);
402 559
            for ($i = 0; $i < count($pars); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
403
                $name = strtolower($funcDesc['params'][$i]['name']);
404 559
                if (isset($funcDesc['paramDocs'][$name]['type'])) {
405
                    $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']);
406 559
                } else {
407 559
                    $sig[] = Value::$xmlrpcValue;
408
                }
409
                $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : '';
410
            }
411 559
            $sigs[] = $sig;
412 559
            $sigsDocs[] = $pSig;
413
        }
414
415
        return array(
416
            'sigs' => $sigs,
417
            'sigsDocs' => $sigsDocs
418
        );
419
    }
420
421
    /**
422
     * Creates a closure that will execute $callable
423
     *
424
     * @param $callable
425
     * @param array $extraOptions
426
     * @param string $plainFuncName
427 559
     * @param array $funcDesc
428
     * @return \Closure
429
     *
430
     * @todo validate params? In theory all validation is left to the dispatch map...
431
     * @todo add support for $catchWarnings
432
     */
433
    protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
434
    {
435 108
        /**
436 108
         * @param Request $req
437 108
         *
438 108
         * @return mixed
439
         */
440
        $function = function($req) use($callable, $extraOptions, $funcDesc)
441
        {
442 108
            $encoderClass = static::$namespace.'Encoder';
443 108
            $responseClass = static::$namespace.'Response';
444 108
            $valueClass = static::$namespace.'Value';
445 108
446
            // validate number of parameters received
447
            // this should be optional really, as we assume the server does the validation
448
            $minPars = count($funcDesc['params']);
449
            $maxPars = $minPars;
450
            foreach ($funcDesc['params'] as $i => $param) {
451 108
                if ($param['isoptional']) {
452 108
                    // this particular parameter is optional. We assume later ones are as well
453
                    $minPars = $i;
454
                    break;
455
                }
456 108
            }
457 108
            $numPars = $req->getNumParams();
458 108
            if ($numPars < $minPars || $numPars > $maxPars) {
459
                return new $responseClass(0, 3, 'Incorrect parameters passed to method');
460
            }
461 108
462
            $encoder = new $encoderClass();
463 108
            $options = array();
464
            if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) {
465 108
                $options[] = 'decode_php_objs';
466 108
            }
467
            $params = $encoder->decode($req, $options);
468
469 108
            $result = call_user_func_array($callable, $params);
470 108
471 1
            if (! is_a($result, $responseClass)) {
472
                // q: why not do the same for int, float, bool, string?
473
                if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
474 108
                    $result = new $valueClass($result, $funcDesc['returns']);
475
                } else {
476 108
                    $options = array();
477
                    if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) {
478
                        $options[] = 'encode_php_objs';
479 108
                    }
480 559
                    if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) {
481
                        $options[] = 'null_extension';
482 559
                    }
483
484
                    $result = $encoder->encode($result, $options);
485
                }
486
                $result = new $responseClass($result);
487
            }
488
489
            return $result;
490
        };
491 643
492
        return $function;
493
    }
494
495 643
    /**
496
     * Return a name for a new function, based on $callable, insuring its uniqueness
497 643
     * @param mixed $callable a php callable, or the name of an xml-rpc method
498 622
     * @param string $newFuncName when not empty, it is used instead of the calculated version
499 559
     * @return string
500 559
     */
501
    protected function newFunctionName($callable, $newFuncName, $extraOptions)
502 559
    {
503
        // determine name of new php function
504
505 622
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
506
507
        if ($newFuncName == '') {
508 622
            if (is_array($callable)) {
509 622
                if (is_string($callable[0])) {
510 622
                    $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable);
511
                } else {
512
                    $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1];
513
                }
514 22
            } else {
515
                if ($callable instanceof \Closure) {
516
                    $xmlrpcFuncName = "{$prefix}_closure";
517 643
                } else {
518 620
                    $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
519
                        array('_', ''), $callable);
520
                    $xmlrpcFuncName = "{$prefix}_$callable";
521 643
                }
522
            }
523
        } else {
524
            $xmlrpcFuncName = $newFuncName;
525
        }
526
527
        while (function_exists($xmlrpcFuncName)) {
528
            $xmlrpcFuncName .= 'x';
529
        }
530
531
        return $xmlrpcFuncName;
532
    }
533
534 559
    /**
535
     * @param $callable
536 559
     * @param string $newFuncName
537
     * @param array $extraOptions
538 559
     * @param string $plainFuncName
539 559
     * @param array $funcDesc
540 559
     * @return string
541
     */
542 559
    protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
543 559
    {
544 559
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
545 559
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
546 559
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
547
        $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : '';
548 559
549
        $i = 0;
550
        $parsVariations = array();
551
        $pars = array();
552
        $pNum = count($funcDesc['params']);
553 559
        foreach ($funcDesc['params'] as $param) {
554 559
555 559
            if ($param['isoptional']) {
556
                // this particular parameter is optional. save as valid previous list of parameters
557 559
                $parsVariations[] = $pars;
558
            }
559
560
            $pars[] = "\$params[$i]";
561 559
            $i++;
562
            if ($i == $pNum) {
563
                // last allowed parameters combination
564
                $parsVariations[] = $pars;
565
            }
566
        }
567 559
568 559
        if (count($parsVariations) == 0) {
569
            // only known good synopsis = no parameters
570
            $parsVariations[] = array();
571
            $minPars = 0;
572
            $maxPars = 0;
573 559
        } else {
574 559
            $minPars = count($parsVariations[0]);
575
            $maxPars = count($parsVariations[count($parsVariations)-1]);
576 559
        }
577 559
578
        // build body of new function
579
580 559
        $innerCode = "  \$paramCount = \$req->getNumParams();\n";
581
        $innerCode .= "  if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n";
582
583
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
584
        if ($decodePhpObjects) {
585 559
            $innerCode .= "  \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n";
586 559
        } else {
587 559
            $innerCode .= "  \$params = \$encoder->decode(\$req);\n";
588 559
        }
589
590 559
        // since we are building source code for later use, if we are given an object instance,
591
        // we go out of our way and store a pointer to it in a static class var...
592 559
        if (is_array($callable) && is_object($callable[0])) {
593 559
            static::holdObject($newFuncName, $callable[0]);
594 559
            $class = get_class($callable[0]);
595
            if ($class[0] !== '\\') {
596
                $class = '\\' . $class;
597 559
            }
598 559
            $innerCode .= "  /// @var $class \$obj\n";
599
            $innerCode .= "  \$obj = PhpXmlRpc\\Wrapper::getHeldObject('$newFuncName');\n";
600
            $realFuncName = '$obj->' . $callable[1];
601 559
        } else {
602
            $realFuncName = $plainFuncName;
603
        }
604 559
        foreach ($parsVariations as $i => $pars) {
605
            $innerCode .= "  if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n";
606
            if ($i < (count($parsVariations) - 1))
607
                $innerCode .= "  else\n";
608
        }
609
        $innerCode .= "  if (is_a(\$retVal, '" . static::$namespace . "Response'))\n    return \$retVal;\n  else\n";
610
        /// q: why not do the same for int, float, bool, string?
611 559
        if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
612
            $innerCode .= "    return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));";
613 559
        } else {
614
            $encodeOptions = array();
615
            if ($encodeNulls) {
616
                $encodeOptions[] = 'null_extension';
617
            }
618
            if ($encodePhpObjects) {
619
                $encodeOptions[] = 'encode_php_objs';
620
            }
621
622
            if ($encodeOptions) {
623
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" .
624
                    implode("', '", $encodeOptions) . "')));";
625
            } else {
626
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));";
627
            }
628
        }
629 559
        // shall we exclude functions returning by ref?
630
        // if ($func->returnsReference())
631 559
        //     return false;
632 559
633
        $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" .
634 559
            "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}";
635 559
636 559
        return $code;
637 559
    }
638 559
639 559
    /**
640 559
     * Given a user-defined PHP class or php object, map its methods onto a list of
641 559
     * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server
642
     * object and called from remote clients (as well as their corresponding signature info).
643 559
     *
644
     * @param string|object $className the name of the class whose methods are to be exposed as xml-rpc methods, or an object instance of that class
645 559
     * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus
646 559
     *                            - 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
647
     *                            - string method_filter  a regexp used to filter methods to wrap based on their names
648
     *                            - string prefix         used for the names of the xml-rpc methods created.
649
     *                            - string replace_class_name use to completely replace the class name with the prefix in the generated method names. e.g. instead of \Some\Namespace\Class.method use prefixmethod
650
     * @return array|false false on failure, or on array useable for the dispatch map
651
     *
652
     * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on
653 559
     *       each invocation, for the case where all the generated functions will be saved as methods of a class
654
     */
655
    public function wrapPhpClass($className, $extraOptions = array())
656
    {
657
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
658
        $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
659
660
        $results = array();
661
        $mList = get_class_methods($className);
662 559
        foreach ($mList as $mName) {
663
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
664 559
                $func = new \ReflectionMethod($className, $mName);
665 559
                if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
666
                    if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) ||
667
                        (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
668 559
                    ) {
669 559
                        $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions);
670
671
                        if ($methodWrap) {
672
                            $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap;
673 559
                        }
674
                    }
675
                }
676
            }
677
        }
678
679
        return $results;
680
    }
681
682
    /**
683
     * @param string|object $className
684
     * @param string $classMethod
685
     * @param array $extraOptions
686
     * @return string
687
     *
688
     * @todo php allows many more characters in identifiers than the xml-rpc spec does. We should make sure to
689
     *       replace those (while trying to make sure we are not running in collisions)
690
     */
691
    protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array())
692
    {
693
        if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) {
694
            return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . $classMethod;
695
        }
696
697
        if (is_object($className)) {
698
            $realClassName = get_class($className);
699
        } else {
700
            $realClassName = $className;
701
        }
702
        return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . "$realClassName.$classMethod";
703
    }
704
705
    /**
706
     * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results
707
     * using native php types for both arguments and results. The generated php function will return a Response
708
     * object for failed xml-rpc calls.
709
     *
710
     * Known limitations:
711
     * - server must support system.methodSignature for the target xml-rpc method
712
     * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures
713
     *   differ only by number of params and not by type, but it would be more complication than we can spare time for)
714
     * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to
715
     *   the php function if these are structs or arrays whose (sub)members include values of type base64
716
     *
717
     * Notes: the connection properties of the given client will be copied and reused for the connection used during
718
     * the call to the generated php function.
719
     * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation
720
     * and an xmlrpc-connection opened+closed.
721 109
     * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful
722
     * for debugging purposes.
723 109
     *
724
     * @param Client $client an xml-rpc client set up correctly to communicate with target server
725 109
     * @param string $methodName the xml-rpc method to be mapped to a php function
726
     * @param array $extraOptions array of options that specify conversion details. Valid options include
727 109
     *                            - integer signum              the index of the method signature to use in mapping (if
728 109
     *                                                          method exposes many sigs)
729 24
     *                            - integer timeout             timeout (in secs) to be used when executing function/calling remote method
730
     *                            - string  protocol            'http' (default), 'http11', 'https', 'h2' or 'h2c'
731
     *                            - string  new_function_name   the name of php function to create, when return_source is used.
732 86
     *                                                          If unspecified, lib will pick an appropriate name
733 1
     *                            - string  return_source       if true return php code w. function definition instead of
734
     *                                                          the function itself (closure)
735
     *                            - bool    encode_nulls        if true, use `<nil/>` elements instead of empty string xml-rpc
736
     *                                                          values for php null values
737 85
     *                            - bool    encode_php_objs     let php objects be sent to server using the 'improved' xml-rpc
738
     *                                                          notation, so server can deserialize them as php objects
739 85
     *                            - bool    decode_php_objs     --- WARNING !!! possible security hazard. only use it with
740
     *                                                          trusted servers ---
741 85
     *                            - mixed   return_on_fault     a php value to be returned when the xml-rpc call fails/returns
742
     *                                                          a fault response (by default the Response object is returned
743
     *                                                          in this case).  If a string is used, '%faultCode%' and
744
     *                                                          '%faultString%' tokens  will be substituted with actual error values
745
     *                            - bool    throw_on_fault      if true, throw an exception instead of returning a Response
746
     *                                                          in case of errors/faults;
747 85
     *                                                          if a string, do the same and assume it is the exception class to throw
748
     *                            - bool    debug               set it to 1 or 2 to see debug results of querying server for
749 85
     *                                                          method synopsis
750
     *                            - int     simple_client_copy  set it to 1 to have a lightweight copy of the $client object
751
     *                                                          made in the generated code (only used when return_source = true)
752
     * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true
753
     *
754
     * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it'
755
     * @todo if we can not retrieve method signature, create a php function with varargs
756
     * @todo if caller did not specify a specific sig, shall we support all of them?
757
     *       It might be hard (hence slow) to match based on type and number of arguments...
758
     * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
759
     * @todo allow creating functions which have an extra `$debug=0` parameter
760 109
     */
761
    public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array())
762 109
    {
763 109
        $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : '';
764 109
765 109
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
766
767 109
        $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions);
768 109
        if (!$mSig) {
769 109
            return false;
770 109
        }
771
772 109
        if ($buildIt) {
773 109
            return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig);
774 109
        } else {
775 109
            // if in 'offline' mode, retrieve method description too.
776 109
            // in online mode, favour speed of operation
777 24
            $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions);
778 24
779
            $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions);
780
781 86
            $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc);
782 86
783 85
            $results['function'] = $newFuncName;
784 85
785
            return $results;
786
        }
787 86
    }
788
789
    /**
790
     * Retrieves an xml-rpc method signature from a server which supports system.methodSignature
791
     * @param Client $client
792 86
     * @param string $methodName
793
     * @param array $extraOptions
794
     * @return false|array
795
     */
796
    protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array())
797
    {
798
        $reqClass = static::$namespace . 'Request';
799
        $valClass = static::$namespace . 'Value';
800
        $decoderClass = static::$namespace . 'Encoder';
801 85
802
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
803 85
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
804 85
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
805 85
        $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0;
806
807 85
        $req = new $reqClass('system.methodSignature');
808 85
        $req->addParam(new $valClass($methodName));
809 85
        $origDebug = $client->getOption(Client::OPT_DEBUG);
810
        $client->setDebug($debug);
811 85
        /// @todo move setting of timeout, protocol to outside the send() call
812
        $response = $client->send($req, $timeout, $protocol);
813 85
        $client->setDebug($origDebug);
814 85
        if ($response->faultCode()) {
815 85
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName);
816 85
            return false;
817 85
        }
818 85
819 85
        $mSig = $response->value();
820 85
        /// @todo what about return xml?
821
        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
822
            $decoder = new $decoderClass();
823
            $mSig = $decoder->decode($mSig);
824 85
        }
825
826
        if (!is_array($mSig) || count($mSig) <= $sigNum) {
827
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName);
828
            return false;
829
        }
830
831
        return $mSig[$sigNum];
832
    }
833
834
    /**
835
     * @param Client $client
836 1
     * @param string $methodName
837
     * @param array $extraOptions
838
     * @return string in case of any error, an empty string is returned, no warnings generated
839 1
     */
840
    protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array())
841
    {
842 1
        $reqClass = static::$namespace . 'Request';
843 1
        $valClass = static::$namespace . 'Value';
844 1
845 1
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
846 1
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
847
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
848
849
        $mDesc = '';
850 1
851
        $req = new $reqClass('system.methodHelp');
852
        $req->addParam(new $valClass($methodName));
853 1
        $origDebug = $client->getOption(Client::OPT_DEBUG);
854 1
        $client->setDebug($debug);
855 1
        /// @todo move setting of timeout, protocol to outside the send() call
856 1
        $response = $client->send($req, $timeout, $protocol);
857
        $client->setDebug($origDebug);
858 1
        if (!$response->faultCode()) {
859 1
            $mDesc = $response->value();
860 1
            if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
861
                $mDesc = $mDesc->scalarVal();
862
            }
863 1
        }
864 1
865
        return $mDesc;
866
    }
867
868
    /**
869
     * @param Client $client
870
     * @param string $methodName
871 1
     * @param array $extraOptions @see wrapXmlrpcMethod
872 1
     * @param array $mSig
873 1
     * @return \Closure
874 1
     *
875 1
     * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case?
876
     */
877
    protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig)
878 1
    {
879 1
        // we clone the client, so that we can modify it a bit independently of the original
880 1
        $clientClone = clone $client;
881
        $function = function() use($clientClone, $methodName, $extraOptions, $mSig)
882
        {
883 1
            $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
884 1
            $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
885
            $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
886
            $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
887
            $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
888
            $throwFault = false;
889 1
            $decodeFault = false;
890
            $faultResponse = null;
891
            if (isset($extraOptions['throw_on_fault'])) {
892
                $throwFault = $extraOptions['throw_on_fault'];
893
            } else if (isset($extraOptions['return_on_fault'])) {
894
                $decodeFault = true;
895 1
                $faultResponse = $extraOptions['return_on_fault'];
896
            }
897 1
898 1
            $reqClass = static::$namespace . 'Request';
899 1
            $encoderClass = static::$namespace . 'Encoder';
900
            $valueClass = static::$namespace . 'Value';
901
902
            $encoder = new $encoderClass();
903
            $encodeOptions = array();
904
            if ($encodePhpObjects) {
905
                $encodeOptions[] = 'encode_php_objs';
906
            }
907
            if ($encodeNulls) {
908
                $encodeOptions[] = 'null_extension';
909
            }
910
            $decodeOptions = array();
911 1
            if ($decodePhpObjects) {
912
                $decodeOptions[] = 'decode_php_objs';
913 1
            }
914
915 1
            /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not...
916
917
            // support one extra parameter: debug
918
            $maxArgs = count($mSig)-1; // 1st element is the return type
919
            $currentArgs = func_get_args();
920
            if (func_num_args() == ($maxArgs+1)) {
921
                $debug = array_pop($currentArgs);
922
                $clientClone->setDebug($debug);
923
            }
924
925
            $xmlrpcArgs = array();
926
            foreach ($currentArgs as $i => $arg) {
927 85
                if ($i == $maxArgs) {
928
                    break;
929 85
                }
930 85
                $pType = $mSig[$i+1];
931 85
                if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
932 85
                    $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
933 85
                ) {
934 85
                    // by building directly xml-rpc values when type is known and scalar (instead of encode() calls),
935 85
                    // we make sure to honour the xml-rpc signature
936
                    $xmlrpcArgs[] = new $valueClass($arg, $pType);
937
                } else {
938
                    $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions);
939 85
                }
940 85
            }
941
942
            $req = new $reqClass($methodName, $xmlrpcArgs);
943 85
            // use this to get the maximum decoding flexibility
944
            $clientClone->setOption(Client::OPT_RETURN_TYPE, 'xmlrpcvals');
945 85
            $resp = $clientClone->send($req, $timeout, $protocol);
946 85
            if ($resp->faultcode()) {
947
                if ($throwFault) {
948 64
                    if (is_string($throwFault)) {
949 64
                        throw new $throwFault($resp->faultString(), $resp->faultCode());
950 64
                    } else {
951 64
                        throw new \PhpXmlRpc\Exception($resp->faultString(), $resp->faultCode());
952
                    }
953
                } else if ($decodeFault) {
954 22
                    if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) ||
955 22
                            (strpos($faultResponse, '%faultString%') !== false))) {
956
                        $faultResponse = str_replace(array('%faultCode%', '%faultString%'),
957 85
                            array($resp->faultCode(), $resp->faultString()), $faultResponse);
958
                    }
959 85
                    return $faultResponse;
960
                } else {
961 85
                    return $resp;
962
                }
963
            } else {
964
                return $encoder->decode($resp->value(), $decodeOptions);
965
            }
966
        };
967 85
968 85
        return $function;
969 85
    }
970 85
971 64
    /**
972 64
     * @internal made public just for Debugger usage
973 64
     *
974 64
     * @param Client $client
975
     * @param string $methodName
976
     * @param array $extraOptions @see wrapXmlrpcMethod
977 64
     * @param string $newFuncName
978
     * @param array $mSig
979
     * @param string $mDesc
980
     * @return string[] keys: source, docstring
981
     */
982
    public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='')
983
    {
984
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
985 64
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
986 64
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
987
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
988 85
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
989 64
        $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
990 64
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
991
        $throwFault = false;
992 85
        $decodeFault = false;
993 85
        $faultResponse = null;
994
        if (isset($extraOptions['throw_on_fault'])) {
995 85
            $throwFault = $extraOptions['throw_on_fault'];
996 85
        } else if (isset($extraOptions['return_on_fault'])) {
997
            $decodeFault = true;
998
            $faultResponse = $extraOptions['return_on_fault'];
999
        }
1000
1001
        $code = "function $newFuncName(";
1002
        if ($clientCopyMode < 2) {
1003 85
            // client copy mode 0 or 1 == full / partial client copy in emitted code
1004
            $verbatimClientCopy = !$clientCopyMode;
1005 85
            $innerCode = '  ' . str_replace("\n", "\n  ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
1006 22
            $innerCode .= "\$client->setDebug(\$debug);\n";
1007
            $this_ = '';
1008 64
        } else {
1009
            // client copy mode 2 == no client copy in emitted code
1010
            $innerCode = '';
1011 85
            $this_ = 'this->';
1012
        }
1013 85
        $innerCode .= "  \$req = new " . static::$namespace . "Request('$methodName');\n";
1014
1015
        if ($mDesc != '') {
1016
            // take care that PHP comment is not terminated unwillingly by method description
1017
            /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags...
1018
            $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n";
1019
        } else {
1020
            $mDesc = "/**\n * Function $newFuncName.\n";
1021
        }
1022
1023
        // param parsing
1024
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
1025
        $plist = array();
1026
        $pCount = count($mSig);
1027
        for ($i = 1; $i < $pCount; $i++) {
1028
            $plist[] = "\$p$i";
1029
            $pType = $mSig[$i];
1030
            if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
1031
                $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
1032
            ) {
1033
                // only build directly xml-rpc values when type is known and scalar
1034 22
                $innerCode .= "  \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n";
1035
            } else {
1036 22
                if ($encodePhpObjects || $encodeNulls) {
1037 22
                    $encOpts = array();
1038 22
                    if ($encodePhpObjects) {
1039 22
                        $encOpts[] = 'encode_php_objs';
1040 22
                    }
1041 22
                    if ($encodeNulls) {
1042 22
                        $encOpts[] = 'null_extension';
1043 22
                    }
1044 22
1045 22
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n";
1046
                } else {
1047 22
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i);\n";
1048 22
                }
1049
            }
1050 22
            $innerCode .= "  \$req->addParam(\$p$i);\n";
1051 22
            $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n";
1052 22
        }
1053
        if ($clientCopyMode < 2) {
1054
            $plist[] = '$debug = 0';
1055
            $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n";
1056
        }
1057 22
        $plist = implode(', ', $plist);
1058 22
        $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]);
1059 22
        if ($throwFault) {
1060 22
            $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\PhpXmlRpc\\Exception');
1061
        } else if ($decodeFault) {
1062 22
            $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)";
1063
        } else {
1064
            $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)";
1065
        }
1066
        $mDesc .= "\n */\n";
1067
1068 22
        /// @todo move setting of timeout, protocol to outside the send() call
1069
        $innerCode .= "  \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
1070
        if ($throwFault) {
1071 22
            if (!is_string($throwFault)) {
1072 22
                $throwFault = '\\PhpXmlRpc\\Exception';
1073
            }
1074 22
            $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())";
1075 21
        } else if ($decodeFault) {
1076
            if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
1077
                $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
1078
            } else {
1079 22
                $respCode = 'return ' . var_export($faultResponse, true);
1080 22
            }
1081 22
        } else {
1082 22
            $respCode = 'return $res';
1083
        }
1084 22
        if ($decodePhpObjects) {
1085 22
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
1086 22
        } else {
1087 22
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());";
1088 22
        }
1089 22
1090 22
        $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n";
1091
1092
        return array('source' => $code, 'docstring' => $mDesc);
1093 22
    }
1094 22
1095
    /**
1096 22
     * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote
1097 22
     * server as own methods.
1098 22
     * For a slimmer alternative, see the code in demo/client/proxy.php.
1099 22
     * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but
1100 22
     * we do not support them yet...
1101
     *
1102
     * @see wrapXmlrpcMethod for more details.
1103 22
     *
1104
     * @param Client $client the client obj all set to query the desired server
1105
     * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus
1106
     *                            - string method_filter      regular expression
1107
     *                            - string new_class_name
1108
     *                            - string prefix
1109 22
     *                            - bool   simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class
1110 22
     * @return string|array|false false on error, the name of the created class if all ok or an array with code, class name and comments (if the appropriate option is set in extra_options)
1111 22
     *
1112 22
     * @todo add support for anonymous classes in the 'buildIt' case for php > 7
1113 22
     * @todo add method setDebug() to new class, to enable/disable debugging
1114 22
     * @todo optimization - move the generated Encoder instance to be a property of the created class, instead of creating
1115
     *                      it on every generated method invocation
1116
     */
1117
    public function wrapXmlrpcServer($client, $extraOptions = array())
1118
    {
1119
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
1120
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
1121
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
1122
        $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : '';
1123
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
1124
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
1125
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
1126
        $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true;
1127
        $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false;
1128
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
1129
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
1130
1131
        $reqClass = static::$namespace . 'Request';
1132
        $decoderClass = static::$namespace . 'Encoder';
1133
1134
        // retrieve the list of methods
1135
        $req = new $reqClass('system.listMethods');
1136
        /// @todo move setting of timeout, protocol to outside the send() call
1137 85
        $response = $client->send($req, $timeout, $protocol);
1138
        if ($response->faultCode()) {
1139 85
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server');
1140 85
1141
            return false;
1142
        }
1143
        $mList = $response->value();
1144 85
        /// @todo what about return_type = xml?
1145 85
        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
1146
            $decoder = new $decoderClass();
1147
            $mList = $decoder->decode($mList);
1148
        }
1149
        if (!is_array($mList) || !count($mList)) {
1150
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server');
1151 85
1152 85
            return false;
1153 85
        }
1154
1155
        // pick a suitable name for the new function, avoiding collisions
1156
        if ($newClassName != '') {
1157
            $xmlrpcClassName = $newClassName;
1158 85
        } else {
1159
            /// @todo direct access to $client->server is now deprecated
1160 85
            $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), array('_', ''),
1161
                $client->server) . '_client';
1162
        }
1163
        while ($buildIt && class_exists($xmlrpcClassName)) {
1164
            $xmlrpcClassName .= 'x';
1165
        }
1166
1167
        $source = "class $xmlrpcClassName\n{\n  public \$client;\n\n";
1168
        $source .= "  function __construct()\n  {\n";
1169
        $source .= '    ' . str_replace("\n", "\n    ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
1170
        $source .= "\$this->client = \$client;\n  }\n\n";
1171
        $opts = array(
1172
            'return_source' => true,
1173
            'simple_client_copy' => 2, // do not produce code to copy the client object
1174
            'timeout' => $timeout,
1175
            'protocol' => $protocol,
1176
            'encode_nulls' => $encodeNulls,
1177
            'encode_php_objs' => $encodePhpObjects,
1178
            'decode_php_objs' => $decodePhpObjects,
1179
            'throw_on_fault' => $throwOnFault,
1180
            'prefix' => $prefix,
1181
        );
1182
1183
        /// @todo build phpdoc for class definition, too
1184
        foreach ($mList as $mName) {
1185
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
1186
                /// @todo this will fail if server exposes 2 methods called f.e. do.something and do_something
1187
                $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1188
                    array('_', ''), $mName);
1189
                $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts);
1190
                if ($methodWrap) {
1191
                    if ($buildIt) {
1192
                        $source .= $methodWrap['source'] . "\n";
1193
1194
                    } else {
1195
                        $source .= '  ' . str_replace("\n", "\n  ", $methodWrap['docstring']);
1196
                        $source .= str_replace("\n", "\n  ", $methodWrap['source']). "\n";
1197
                    }
1198
1199
                } else {
1200
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName);
1201
                }
1202
            }
1203
        }
1204
        $source .= "}\n";
1205
        if ($buildIt) {
1206
            $allOK = 0;
1207
            eval($source . '$allOK=1;');
1208
            if ($allOK) {
1209
                return $xmlrpcClassName;
1210
            } else {
1211
                /// @todo direct access to $client->server is now deprecated
1212
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName .
1213
                    ' to wrap remote server ' . $client->server);
1214
                return false;
1215
            }
1216
        } else {
1217
            return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => '');
1218
        }
1219
    }
1220
1221
    /**
1222
     * Given necessary info, generate php code that will build a client object just like the given one.
1223
     * Take care that no full checking of input parameters is done to ensure that valid php code is emitted.
1224
     * @param Client $client
1225
     * @param bool $verbatimClientCopy when true, copy the whole options of the client, except for 'debug' and 'return_type'
1226
     * @param string $prefix used for the return_type of the created client
1227
     * @param string $namespace
1228
     * @return string
1229
     */
1230
    protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\')
1231
    {
1232
        $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->getUrl()) .
1233
            "');\n";
1234
1235
        // copy all client fields to the client that will be generated runtime
1236
        // (this provides for future expansion or subclassing of client obj)
1237
        if ($verbatimClientCopy) {
1238
            foreach ($client->getOptions() as $opt => $val) {
1239
                if ($opt != 'debug' && $opt != 'return_type') {
1240
                    $val = var_export($val, true);
1241
                    $code .= "\$client->setOption('$opt', $val);\n";
1242
                }
1243
            }
1244
        }
1245
        // only make sure that client always returns the correct data type
1246
        $code .= "\$client->setOption(\PhpXmlRpc\Client::OPT_RETURN_TYPE, '{$prefix}vals');\n";
1247
        return $code;
1248
    }
1249
1250
    /**
1251
     * @param string $index
1252
     * @param object $object
1253
     * @return void
1254
     */
1255
    public static function holdObject($index, $object)
1256
    {
1257
        self::$objHolder[$index] = $object;
1258
    }
1259
1260
    /**
1261
     * @param string $index
1262
     * @return object
1263
     * @throws ValueErrorException
1264
     */
1265
    public static function getHeldObject($index)
1266
    {
1267
        if (isset(self::$objHolder[$index])) {
1268
            return self::$objHolder[$index];
1269
        }
1270
1271
        throw new ValueErrorException("No object held for index '$index'");
1272
    }
1273
}
1274