Issues (325)

src/Wrapper.php (11 issues)

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
    /** @var string */
37
    protected static $prefix = 'xmlrpc';
38
39
    /** @var null|string set to a namespaced class. If empty, static::$namespace . 'Response' will be used */
40
    protected static $allowedResponseClass = null;
41
42
    /**
43
     * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
44
     * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
45
     * Notes:
46
     * - for php 'resource' types returns empty string, since resources cannot be serialized;
47
     * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs
48
     * - for php arrays always return array, even though arrays sometimes serialize as structs...
49
     * - for 'void' and 'null' returns 'undefined'
50
     *
51
     * @param string $phpType
52
     * @return string
53
     *
54
     * @todo support notation `something[]` as 'array'
55
     * @todo check if nil support is enabled when finding null or void (which makes sense in php for return type)
56
     */
57 559
    public function php2XmlrpcType($phpType)
58
    {
59 559
        switch (strtolower($phpType)) {
60 559
            case 'string':
61 559
                return Value::$xmlrpcString;
62 559
            case 'integer':
63 559
            case Value::$xmlrpcInt: // 'int'
64 559
            case Value::$xmlrpcI4:
65 559
            case Value::$xmlrpcI8:
66 559
                return Value::$xmlrpcInt;
67 559
            case Value::$xmlrpcDouble: // 'double'
68
                return Value::$xmlrpcDouble;
69 559
            case 'bool':
70 559
            case Value::$xmlrpcBoolean: // 'boolean'
71 559
            case 'false':
72 559
            case 'true':
73
                return Value::$xmlrpcBoolean;
74 559
            case Value::$xmlrpcArray: // 'array':
75 559
            case 'array[]':
76
                return Value::$xmlrpcArray;
77 559
            case 'object':
78 559
            case Value::$xmlrpcStruct: // 'struct'
79
                return Value::$xmlrpcStruct;
80 559
            case Value::$xmlrpcBase64:
81
                return Value::$xmlrpcBase64;
82 559
            case 'resource':
83
                return '';
84
            default:
85 559
                if (class_exists($phpType)) {
86 559
                    // DateTimeInterface is not present in php 5.4...
87
                    if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) {
88
                        return Value::$xmlrpcDateTime;
89 559
                    }
90
                    return Value::$xmlrpcStruct;
91
                } else {
92 559
                    // unknown: might be any 'extended' xml-rpc type
93
                    return Value::$xmlrpcValue;
94
                }
95
        }
96
    }
97
98
    /**
99
     * Given a string defining a phpxmlrpc type return the corresponding php type.
100
     *
101
     * @param string $xmlrpcType
102
     * @return string
103
     */
104 85
    public function xmlrpc2PhpType($xmlrpcType)
105
    {
106 85
        switch (strtolower($xmlrpcType)) {
107 85
            case 'base64':
108 85
            case 'datetime.iso8601':
109 85
            case 'string':
110 64
                return Value::$xmlrpcString;
111 85
            case 'int':
112 22
            case 'i4':
113 22
            case 'i8':
114 64
                return 'integer';
115 22
            case 'struct':
116 22
            case 'array':
117
                return 'array';
118 22
            case 'double':
119
                return 'float';
120 22
            case 'undefined':
121 22
                return 'mixed';
122
            case 'boolean':
123
            case 'null':
124
            default:
125
                // unknown: might be any xml-rpc type
126
                return strtolower($xmlrpcType);
127
        }
128
    }
129
130
    /**
131
     * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an
132
     * xml-rpc server object and called from remote clients (as well as its corresponding signature info).
133
     *
134
     * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the
135
     * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as
136
     * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to
137
     * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by
138
     * the lib, while datetime values are passed around as strings)
139
     *
140
     * Known limitations:
141
     * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving
142
     *   number/type of params for those)
143
     * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of
144
     *   those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php
145
     *   objects will be rebuilt.
146
     *   In short: php objects can be serialized, too (except for their resource members), using this function.
147
     *   Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard
148
     *   attribute on struct element tags)
149
     *
150
     * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions
151
     * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and
152
     * $exception_handling properties.
153
     *
154
     * @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
0 ignored issues
show
The type Callable was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
155
     * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true
156
     * @param array $extraOptions (optional) array of options for conversion. valid values include:
157
     *                            - bool return_source     when true, php code w. function definition will be returned, instead of a closure
158
     *                            - bool encode_nulls      let php objects be sent to server using <nil> elements instead of empty strings
159
     *                            - 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
160
     *                            - bool decode_php_objs   --- WARNING !!! possible security hazard. only use it with trusted servers ---
161
     *                            - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked
162
     * @return array|false false on error, or an array containing the name of the new php function,
163
     *                     its signature and docs, to be used in the server dispatch map
164
     *
165
     * @todo decide how to deal with params passed by ref in function definition: bomb out or allow?
166
     * @todo finish using phpdoc info to build method sig if all params are named but out of order
167
     * @todo add a check for params of 'resource' type
168
     * @todo add some error logging when returning false?
169
     * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
170
     * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
171
     * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
172
     * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway
173
     */
174
    public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array())
175
    {
176
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
177 559
178
        if (is_string($callable) && strpos($callable, '::') !== false) {
0 ignored issues
show
The condition is_string($callable) is always false.
Loading history...
179 559
            $callable = explode('::', $callable);
180
        }
181 559
        if (is_array($callable)) {
0 ignored issues
show
The condition is_array($callable) is always false.
Loading history...
182 559
            if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) {
183
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong');
184 559
                return false;
185 559
            }
186
            if (is_string($callable[0])) {
187
                $plainFuncName = implode('::', $callable);
188
            } elseif (is_object($callable[0])) {
189 559
                $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
190 559
            }
191 559
            $exists = method_exists($callable[0], $callable[1]);
192 559
        } else if ($callable instanceof \Closure) {
193
            // we do not support creating code which wraps closures, as php does not allow to serialize them
194 559
            if (!$buildIt) {
195 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code');
196
                return false;
197 559
            }
198
199
            $plainFuncName = 'Closure';
200
            $exists = true;
201
        } else {
202 559
            $plainFuncName = $callable;
203 559
            $exists = function_exists($callable);
204
        }
205 559
206 559
        if (!$exists) {
207
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $plainFuncName does not seem to be defined for all execution paths leading up to this point.
Loading history...
208
            return false;
209 559
        }
210
211
        $funcDesc = $this->introspectFunction($callable, $plainFuncName);
212
        if (!$funcDesc) {
213
            return false;
214 559
        }
215 559
216
        $funcSigs = $this->buildMethodSignatures($funcDesc);
217
218
        if ($buildIt) {
219 559
            $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc);
220
        } else {
221 559
            $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions);
222 559
            $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc);
223
        }
224 559
225 559
        $ret = array(
226
            'function' => $callable,
227
            'signature' => $funcSigs['sigs'],
228
            'docstring' => $funcDesc['desc'],
229 559
            'signature_docs' => $funcSigs['sigsDocs'],
230 559
        );
231 559
        if (!$buildIt) {
232 559
            $ret['function'] = $newFuncName;
233
            $ret['source'] = $code;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $code does not seem to be defined for all execution paths leading up to this point.
Loading history...
234 559
        }
235 559
        return $ret;
236 559
    }
237
238 559
    /**
239
     * Introspect a php callable and its phpdoc block and extract information about its signature
240
     *
241
     * @param callable $callable
242
     * @param string $plainFuncName
243
     * @return array|false
244
     */
245
    protected function introspectFunction($callable, $plainFuncName)
246
    {
247
        // start to introspect PHP code
248 559
        if (is_array($callable)) {
249
            $func = new \ReflectionMethod($callable[0], $callable[1]);
250
            if ($func->isPrivate()) {
251 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
252 559
                return false;
253 559
            }
254
            if ($func->isProtected()) {
255
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
256
                return false;
257 559
            }
258
            if ($func->isConstructor()) {
259
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
260
                return false;
261 559
            }
262
            if ($func->isDestructor()) {
263
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
264
                return false;
265 559
            }
266
            if ($func->isAbstract()) {
267
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName);
268
                return false;
269 559
            }
270
            /// @todo add more checks for static vs. nonstatic?
271
        } else {
272
            $func = new \ReflectionFunction($callable);
273
        }
274
        if ($func->isInternal()) {
275 559
            /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully
276
            ///       reflect internal php functions
277 559
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName);
278
            return false;
279
        }
280
281
        // retrieve parameter names, types and description from javadoc comments
282
283
        // function description
284
        $desc = '';
285
        // type of return val: by default 'any'
286
        $returns = Value::$xmlrpcValue;
287 559
        // desc of return val
288
        $returnsDocs = '';
289 559
        // type + name of function parameters
290
        $paramDocs = array();
291 559
292
        $docs = $func->getDocComment();
293 559
        if ($docs != '') {
294
            $docs = explode("\n", $docs);
295 559
            $i = 0;
296 559
            foreach ($docs as $doc) {
297 559
                $doc = trim($doc, " \r\t/*");
298 559
                if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
299 559
                    if ($desc) {
300 559
                        $desc .= "\n";
301 559
                    }
302 559
                    $desc .= $doc;
303 559
                } elseif (strpos($doc, '@param') === 0) {
304
                    // syntax: @param type $name [desc]
305 559
                    if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) {
306 559
                        $name = strtolower(trim($matches[2]));
307
                        //$paramDocs[$name]['name'] = trim($matches[2]);
308 559
                        $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : '';
309 559
                        $paramDocs[$name]['type'] = $matches[1];
310
                    }
311 559
                    $i++;
312 559
                } elseif (strpos($doc, '@return') === 0) {
313
                    // syntax: @return type [desc]
314 559
                    if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
315 559
                        $returns = $matches[1];
316
                        if (isset($matches[2])) {
317 559
                            $returnsDocs = trim($matches[2]);
318 559
                        }
319 559
                    }
320 559
                }
321
            }
322
        }
323
324
        // for php 7+, we can take advantage of type declarations!
325
        if (method_exists($func, 'getReturnType')) {
326
            $returnType = $func->getReturnType();
327
            if ($returnType !== null) {
328 559
/// @todo
329 559
            }
330 559
        }
331 559
332 559
        // execute introspection of actual function prototype
333 559
        $params = array();
334 559
        $i = 0;
335
        foreach ($func->getParameters() as $paramObj) {
336
            $params[$i] = array();
337
            $params[$i]['name'] = '$' . $paramObj->getName();
338 559
            $params[$i]['isoptional'] = $paramObj->isOptional();
339 559
            if (method_exists($paramObj, 'getType')) {
340 559
                $paramType = $paramObj->getType();
341 559
                if ($paramType !== null) {
342 559
/// @todo
343 559
                }
344
            }
345
            $i++;
346
        }
347
348
        return array(
349
            'desc' => $desc,
350
            'docs' => $docs,
351
            'params' => $params, // array, positionally indexed
352
            'paramDocs' => $paramDocs, // array, indexed by name
353
            'returns' => $returns,
354
            'returnsDocs' =>$returnsDocs,
355
        );
356
    }
357 559
358
    /**
359 559
     * Given the method description given by introspection, create method signature data
360 559
     *
361 559
     * @param array $funcDesc as generated by self::introspectFunction()
362 559
     * @return array
363 559
     *
364
     * @todo support better docs with multiple types separated by pipes by creating multiple signatures
365
     *       (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences,
366
     *       but it makes a lot of sense in a php >= 8 world)
367
     */
368
    protected function buildMethodSignatures($funcDesc)
369
    {
370
        $i = 0;
371
        $parsVariations = array();
372
        $pars = array();
373 559
        $pNum = count($funcDesc['params']);
374
        foreach ($funcDesc['params'] as $param) {
375
            /* // match by name real param and documented params
376
            $name = strtolower($param['name']);
377
            if (!isset($funcDesc['paramDocs'][$name])) {
378 559
                $funcDesc['paramDocs'][$name] = array();
379 559
            }
380 559
            if (!isset($funcDesc['paramDocs'][$name]['type'])) {
381
                $funcDesc['paramDocs'][$name]['type'] = 'mixed';
382 559
            }*/
383
384
            if ($param['isoptional']) {
385
                // this particular parameter is optional. save as valid previous list of parameters
386 559
                $parsVariations[] = $pars;
387
            }
388 559
389
            $pars[] = "\$p$i";
390
            $i++;
391 559
            if ($i == $pNum) {
392 559
                // last allowed parameters combination
393 559
                $parsVariations[] = $pars;
394
            }
395 559
        }
396 559
397 559
        if (count($parsVariations) == 0) {
398 559
            // only known good synopsis = no parameters
399 559
            $parsVariations[] = array();
400 559
        }
401
402 559
        $sigs = array();
403
        $sigsDocs = array();
404 559
        foreach ($parsVariations as $pars) {
405
            // build a signature
406 559
            $sig = array($this->php2XmlrpcType($funcDesc['returns']));
407 559
            $pSig = array($funcDesc['returnsDocs']);
408
            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...
409
                $name = strtolower($funcDesc['params'][$i]['name']);
410
                if (isset($funcDesc['paramDocs'][$name]['type'])) {
411 559
                    $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']);
412 559
                } else {
413
                    $sig[] = Value::$xmlrpcValue;
414
                }
415
                $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : '';
416
            }
417
            $sigs[] = $sig;
418
            $sigsDocs[] = $pSig;
419
        }
420
421
        return array(
422
            'sigs' => $sigs,
423
            'sigsDocs' => $sigsDocs
424
        );
425
    }
426
427 559
    /**
428
     * Creates a closure that will execute $callable
429
     *
430
     * @param $callable
431
     * @param array $extraOptions
432
     * @param string $plainFuncName
433
     * @param array $funcDesc
434
     * @return \Closure
435 108
     *
436 108
     * @todo validate params? In theory all validation is left to the dispatch map...
437 108
     * @todo add support for $catchWarnings
438 108
     */
439
    protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
440
    {
441
        /**
442 108
         * @param Request $req
443 108
         *
444 108
         * @return mixed
445 108
         */
446
        $function = function($req) use($callable, $extraOptions, $funcDesc)
447
        {
448
            $encoderClass = static::$namespace.'Encoder';
449
            $responseClass = static::$namespace.'Response';
450
            $valueClass = static::$namespace.'Value';
451 108
            $allowedResponseClass = static::$allowedResponseClass != '' ? static::$allowedResponseClass : $responseClass;
452 108
453
            // validate number of parameters received
454
            // this should be optional really, as we assume the server does the validation
455
            $minPars = count($funcDesc['params']);
456 108
            $maxPars = $minPars;
457 108
            foreach ($funcDesc['params'] as $i => $param) {
458 108
                if ($param['isoptional']) {
459
                    // this particular parameter is optional. We assume later ones are as well
460
                    $minPars = $i;
461 108
                    break;
462
                }
463 108
            }
464
            $numPars = $req->getNumParams();
465 108
            if ($numPars < $minPars || $numPars > $maxPars) {
466 108
                return new $responseClass(0, 3, 'Incorrect parameters passed to method');
467
            }
468
469 108
            $encoder = new $encoderClass();
470 108
            $options = array();
471 1
            if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) {
472
                $options[] = 'decode_php_objs';
473
            }
474 108
            $params = $encoder->decode($req, $options);
475
476 108
            $result = call_user_func_array($callable, $params);
477
478
            if (! is_a($result, $allowedResponseClass)) {
0 ignored issues
show
It seems like $allowedResponseClass can also be of type null; however, parameter $class of is_a() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

478
            if (! is_a($result, /** @scrutinizer ignore-type */ $allowedResponseClass)) {
Loading history...
479 108
                // q: why not do the same for int, float, bool, string?
480 559
                if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
481
                    $result = new $valueClass($result, $funcDesc['returns']);
482 559
                } else {
483
                    $options = array();
484
                    if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) {
485
                        $options[] = 'encode_php_objs';
486
                    }
487
                    if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) {
488
                        $options[] = 'null_extension';
489
                    }
490
491 643
                    $result = $encoder->encode($result, $options);
492
                }
493
                $result = new $responseClass($result);
494
            }
495 643
496
            return $result;
497 643
        };
498 622
499 559
        return $function;
500 559
    }
501
502 559
    /**
503
     * Return a name for a new function, based on $callable, insuring its uniqueness
504
     * @param mixed $callable a php callable, or the name of an xml-rpc method
505 622
     * @param string $newFuncName when not empty, it is used instead of the calculated version
506
     * @return string
507
     */
508 622
    protected function newFunctionName($callable, $newFuncName, $extraOptions)
509 622
    {
510 622
        // determine name of new php function
511
512
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix;
513
514 22
        if ($newFuncName == '') {
515
            if (is_array($callable)) {
516
                if (is_string($callable[0])) {
517 643
                    $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable);
518 620
                } else {
519
                    $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1];
520
                }
521 643
            } else {
522
                if ($callable instanceof \Closure) {
523
                    $xmlrpcFuncName = "{$prefix}_closure";
524
                } else {
525
                    $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
526
                        array('_', ''), $callable);
527
                    $xmlrpcFuncName = "{$prefix}_$callable";
528
                }
529
            }
530
        } else {
531
            $xmlrpcFuncName = $newFuncName;
532
        }
533
534 559
        while (function_exists($xmlrpcFuncName)) {
535
            $xmlrpcFuncName .= 'x';
536 559
        }
537
538 559
        return $xmlrpcFuncName;
539 559
    }
540 559
541
    /**
542 559
     * @param $callable
543 559
     * @param string $newFuncName
544 559
     * @param array $extraOptions
545 559
     * @param string $plainFuncName
546 559
     * @param array $funcDesc
547
     * @return string
548 559
     */
549
    protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
550
    {
551
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
552
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
553 559
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
554 559
        $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : '';
555 559
556
        $i = 0;
557 559
        $parsVariations = array();
558
        $pars = array();
559
        $pNum = count($funcDesc['params']);
560
        foreach ($funcDesc['params'] as $param) {
561 559
562
            if ($param['isoptional']) {
563
                // this particular parameter is optional. save as valid previous list of parameters
564
                $parsVariations[] = $pars;
565
            }
566
567 559
            $pars[] = "\$params[$i]";
568 559
            $i++;
569
            if ($i == $pNum) {
570
                // last allowed parameters combination
571
                $parsVariations[] = $pars;
572
            }
573 559
        }
574 559
575
        if (count($parsVariations) == 0) {
576 559
            // only known good synopsis = no parameters
577 559
            $parsVariations[] = array();
578
            $minPars = 0;
579
            $maxPars = 0;
580 559
        } else {
581
            $minPars = count($parsVariations[0]);
582
            $maxPars = count($parsVariations[count($parsVariations)-1]);
583
        }
584
585 559
        // build body of new function
586 559
587 559
        $innerCode = "  \$paramCount = \$req->getNumParams();\n";
588 559
        $innerCode .= "  if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n";
589
590 559
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
591
        if ($decodePhpObjects) {
592 559
            $innerCode .= "  \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n";
593 559
        } else {
594 559
            $innerCode .= "  \$params = \$encoder->decode(\$req);\n";
595
        }
596
597 559
        // since we are building source code for later use, if we are given an object instance,
598 559
        // we go out of our way and store a pointer to it in a static class var.
599
        // NB: if the code is used in a _separate_ php request, then a class to Wrapper::holdObject() will be necessary!
600
        if (is_array($callable) && is_object($callable[0])) {
601 559
            static::holdObject($newFuncName, $callable[0]);
602
            $class = get_class($callable[0]);
603
            if ($class[0] !== '\\') {
604 559
                $class = '\\' . $class;
605
            }
606
            $innerCode .= "  /// @var $class \$obj\n";
607
            $innerCode .= "  \$obj = " . static::$namespace . "Wrapper::getHeldObject('$newFuncName');\n";
608
            $realFuncName = '$obj->' . $callable[1];
609
        } else {
610
            $realFuncName = $plainFuncName;
611 559
        }
612
        foreach ($parsVariations as $i => $pars) {
613 559
            $innerCode .= "  if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n";
614
            if ($i < (count($parsVariations) - 1))
615
                $innerCode .= "  else\n";
616
        }
617
        $allowedResponseClass = static::$allowedResponseClass != '' ? static::$allowedResponseClass : static::$namespace . 'Response';
618
        $innerCode .= "  if (is_a(\$retVal, '" . $allowedResponseClass . "'))\n    return \$retVal;\n  else\n";
619
        /// q: why not do the same for int, float, bool, string?
620
        if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
621
            $innerCode .= "    return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));";
622
        } else {
623
            $encodeOptions = array();
624
            if ($encodeNulls) {
625
                $encodeOptions[] = 'null_extension';
626
            }
627
            if ($encodePhpObjects) {
628
                $encodeOptions[] = 'encode_php_objs';
629 559
            }
630
631 559
            if ($encodeOptions) {
632 559
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" .
633
                    implode("', '", $encodeOptions) . "')));";
634 559
            } else {
635 559
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));";
636 559
            }
637 559
        }
638 559
        // shall we exclude functions returning by ref?
639 559
        // if ($func->returnsReference())
640 559
        //     return false;
641 559
642
        $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" .
643 559
            "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}";
644
645 559
        return $code;
646 559
    }
647
648
    /**
649
     * Given a user-defined PHP class or php object, map its methods onto a list of
650
     * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server
651
     * object and called from remote clients (as well as their corresponding signature info).
652
     *
653 559
     * @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
654
     * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus
655
     *                            - 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
656
     *                            - string method_filter  a regexp used to filter methods to wrap based on their names
657
     *                            - string prefix         used for the names of the xml-rpc methods created.
658
     *                            - 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
659
     * @return array|false false on failure, or on array useable for the dispatch map
660
     *
661
     * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on
662 559
     *       each invocation, for the case where all the generated functions will be saved as methods of a class
663
     */
664 559
    public function wrapPhpClass($className, $extraOptions = array())
665 559
    {
666
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
667
        $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
668 559
669 559
        $results = array();
670
        $mList = get_class_methods($className);
671
        foreach ($mList as $mName) {
672
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
673 559
                $func = new \ReflectionMethod($className, $mName);
674
                if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
675
                    if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) ||
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: ($func->isStatic() && $m...& is_object($className), Probably Intended Meaning: $func->isStatic() && ($m... is_object($className))
Loading history...
676
                        (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
677
                    ) {
678
                        $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions);
0 ignored issues
show
array($className, $mName) of type array<integer,object|string> is incompatible with the type Callable expected by parameter $callable of PhpXmlRpc\Wrapper::wrapPhpFunction(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

678
                        $methodWrap = $this->wrapPhpFunction(/** @scrutinizer ignore-type */ array($className, $mName), '', $extraOptions);
Loading history...
679
680
                        if ($methodWrap) {
681
                            $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap;
682
                        }
683
                    }
684
                }
685
            }
686
        }
687
688
        return $results;
689
    }
690
691
    /**
692
     * @param string|object $className
693
     * @param string $classMethod
694
     * @param array $extraOptions
695
     * @return string
696
     *
697
     * @todo php allows many more characters in identifiers than the xml-rpc spec does. We should make sure to
698
     *       replace those (while trying to make sure we are not running in collisions)
699
     */
700
    protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array())
701
    {
702
        if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) {
703
            return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . $classMethod;
704
        }
705
706
        if (is_object($className)) {
707
            $realClassName = get_class($className);
708
        } else {
709
            $realClassName = $className;
710
        }
711
        return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . "$realClassName.$classMethod";
712
    }
713
714
    /**
715
     * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results
716
     * using native php types for both arguments and results. The generated php function will return a Response
717
     * object for failed xml-rpc calls.
718
     *
719
     * Known limitations:
720
     * - server must support system.methodSignature for the target xml-rpc method
721 109
     * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures
722
     *   differ only by number of params and not by type, but it would be more complication than we can spare time for)
723 109
     * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to
724
     *   the php function if these are structs or arrays whose (sub)members include values of type base64
725 109
     *
726
     * Notes: the connection properties of the given client will be copied and reused for the connection used during
727 109
     * the call to the generated php function.
728 109
     * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation
729 24
     * and an xmlrpc-connection opened+closed.
730
     * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful
731
     * for debugging purposes.
732 86
     *
733 1
     * @param Client $client an xml-rpc client set up correctly to communicate with target server
734
     * @param string $methodName the xml-rpc method to be mapped to a php function
735
     * @param array $extraOptions array of options that specify conversion details. Valid options include
736
     *                            - integer signum              the index of the method signature to use in mapping (if
737 85
     *                                                          method exposes many sigs)
738
     *                            - integer timeout             timeout (in secs) to be used when executing function/calling remote method
739 85
     *                            - string  protocol            'http' (default), 'http11', 'https', 'h2' or 'h2c'
740
     *                            - string  new_function_name   the name of php function to create, when return_source is used.
741 85
     *                                                          If unspecified, lib will pick an appropriate name
742
     *                            - string  return_source       if true return php code w. function definition instead of
743
     *                                                          the function itself (closure)
744
     *                            - bool    encode_nulls        if true, use `<nil/>` elements instead of empty string xml-rpc
745
     *                                                          values for php null values
746
     *                            - bool    encode_php_objs     let php objects be sent to server using the 'improved' xml-rpc
747 85
     *                                                          notation, so server can deserialize them as php objects
748
     *                            - bool    decode_php_objs     --- WARNING !!! possible security hazard. only use it with
749 85
     *                                                          trusted servers ---
750
     *                            - mixed   return_on_fault     a php value to be returned when the xml-rpc call fails/returns
751
     *                                                          a fault response (by default the Response object is returned
752
     *                                                          in this case). If a string is used, '%faultCode%' and
753
     *                                                          '%faultString%' tokens  will be substituted with actual error values
754
     *                            - bool    throw_on_fault      if true, throw an exception instead of returning a Response
755
     *                                                          in case of errors/faults;
756
     *                                                          if a string, do the same and assume it is the exception class to throw
757
     *                            - bool    debug               set it to 1 or 2 to see debug results of querying server for
758
     *                                                          method synopsis
759
     *                            - int     simple_client_copy  set it to 1 to have a lightweight copy of the $client object
760 109
     *                                                          made in the generated code (only used when return_source = true)
761
     * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true
762 109
     *
763 109
     * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it'
764 109
     * @todo if we can not retrieve method signature, create a php function with varargs
765 109
     * @todo if caller did not specify a specific sig, shall we support all of them?
766
     *       It might be hard (hence slow) to match based on type and number of arguments...
767 109
     * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
768 109
     * @todo allow creating functions which have an extra `$debug=0` parameter
769 109
     */
770 109
    public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array())
771
    {
772 109
        $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : '';
773 109
774 109
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
775 109
776 109
        $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions);
777 24
        if (!$mSig) {
778 24
            return false;
779
        }
780
781 86
        if ($buildIt) {
782 86
            return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig);
783 85
        } else {
784 85
            // if in 'offline' mode, retrieve method description too.
785
            // in online mode, favour speed of operation
786
            $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions);
787 86
788
            $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions);
789
790
            $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc);
791
792 86
            $results['function'] = $newFuncName;
793
794
            return $results;
795
        }
796
    }
797
798
    /**
799
     * Retrieves an xml-rpc method signature from a server which supports system.methodSignature
800
     * @param Client $client
801 85
     * @param string $methodName
802
     * @param array $extraOptions
803 85
     * @return false|array
804 85
     */
805 85
    protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array())
806
    {
807 85
        $reqClass = static::$namespace . 'Request';
808 85
        $valClass = static::$namespace . 'Value';
809 85
        $decoderClass = static::$namespace . 'Encoder';
810
811 85
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
812
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
813 85
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
814 85
        $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0;
815 85
816 85
        $req = new $reqClass('system.methodSignature');
817 85
        $req->addParam(new $valClass($methodName));
818 85
        $origDebug = $client->getOption(Client::OPT_DEBUG);
819 85
        $client->setDebug($debug);
820 85
        /// @todo move setting of timeout, protocol to outside the send() call
821
        $response = $client->send($req, $timeout, $protocol);
822
        $client->setDebug($origDebug);
823
        if ($response->faultCode()) {
824 85
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName);
825
            return false;
826
        }
827
828
        $mSig = $response->value();
829
        /// @todo what about return xml?
830
        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
831
            $decoder = new $decoderClass();
832
            $mSig = $decoder->decode($mSig);
833
        }
834
835
        if (!is_array($mSig) || count($mSig) <= $sigNum) {
836 1
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName);
837
            return false;
838
        }
839 1
840
        return $mSig[$sigNum];
841
    }
842 1
843 1
    /**
844 1
     * @param Client $client
845 1
     * @param string $methodName
846 1
     * @param array $extraOptions
847
     * @return string in case of any error, an empty string is returned, no warnings generated
848
     */
849
    protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array())
850 1
    {
851
        $reqClass = static::$namespace . 'Request';
852
        $valClass = static::$namespace . 'Value';
853 1
854 1
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
855 1
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
856 1
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
857
858 1
        $mDesc = '';
859 1
860 1
        $req = new $reqClass('system.methodHelp');
861
        $req->addParam(new $valClass($methodName));
862
        $origDebug = $client->getOption(Client::OPT_DEBUG);
863 1
        $client->setDebug($debug);
864 1
        /// @todo move setting of timeout, protocol to outside the send() call
865
        $response = $client->send($req, $timeout, $protocol);
866
        $client->setDebug($origDebug);
867
        if (!$response->faultCode()) {
868
            $mDesc = $response->value();
869
            if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
870
                $mDesc = $mDesc->scalarVal();
871 1
            }
872 1
        }
873 1
874 1
        return $mDesc;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $mDesc also could return the type PhpXmlRpc\Value which is incompatible with the documented return type string.
Loading history...
875 1
    }
876
877
    /**
878 1
     * @param Client $client
879 1
     * @param string $methodName
880 1
     * @param array $extraOptions @see wrapXmlrpcMethod
881
     * @param array $mSig
882
     * @return \Closure
883 1
     *
884 1
     * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case?
885
     */
886
    protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig)
887
    {
888
        // we clone the client, so that we can modify it a bit independently of the original
889 1
        $clientClone = $this->cloneClientForClosure($client);
890
        $function = function() use($clientClone, $methodName, $extraOptions, $mSig)
891
        {
892
            $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
893
            $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
894
            $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
895 1
            $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
896
            $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
897 1
            $throwFault = false;
898 1
            $decodeFault = false;
899 1
            $faultResponse = null;
900
            if (isset($extraOptions['throw_on_fault'])) {
901
                $throwFault = $extraOptions['throw_on_fault'];
902
            } else if (isset($extraOptions['return_on_fault'])) {
903
                $decodeFault = true;
904
                $faultResponse = $extraOptions['return_on_fault'];
905
            }
906
907
            $reqClass = static::$namespace . 'Request';
908
            $encoderClass = static::$namespace . 'Encoder';
909
            $valueClass = static::$namespace . 'Value';
910
911 1
            $encoder = new $encoderClass();
912
            $encodeOptions = array();
913 1
            if ($encodePhpObjects) {
914
                $encodeOptions[] = 'encode_php_objs';
915 1
            }
916
            if ($encodeNulls) {
917
                $encodeOptions[] = 'null_extension';
918
            }
919
            $decodeOptions = array();
920
            if ($decodePhpObjects) {
921
                $decodeOptions[] = 'decode_php_objs';
922
            }
923
924
            /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not...
925
926
            // support one extra parameter: debug
927 85
            $maxArgs = count($mSig)-1; // 1st element is the return type
928
            $currentArgs = func_get_args();
929 85
            if (func_num_args() == ($maxArgs+1)) {
930 85
                $debug = array_pop($currentArgs);
931 85
                $clientClone->setDebug($debug);
932 85
            }
933 85
934 85
            $xmlrpcArgs = array();
935 85
            foreach ($currentArgs as $i => $arg) {
936
                if ($i == $maxArgs) {
937
                    break;
938
                }
939 85
                $pType = $mSig[$i+1];
940 85
                if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
941
                    $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
942
                ) {
943 85
                    // by building directly xml-rpc values when type is known and scalar (instead of encode() calls),
944
                    // we make sure to honour the xml-rpc signature
945 85
                    $xmlrpcArgs[] = new $valueClass($arg, $pType);
946 85
                } else {
947
                    $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions);
948 64
                }
949 64
            }
950 64
951 64
            $req = new $reqClass($methodName, $xmlrpcArgs);
952
            // use this to get the maximum decoding flexibility
953
            $clientClone->setOption(Client::OPT_RETURN_TYPE, 'xmlrpcvals');
954 22
            $resp = $clientClone->send($req, $timeout, $protocol);
955 22
            if ($resp->faultcode()) {
956
                if ($throwFault) {
957 85
                    if (is_string($throwFault)) {
958
                        throw new $throwFault($resp->faultString(), $resp->faultCode());
959 85
                    } else {
960
                        throw new \PhpXmlRpc\Exception($resp->faultString(), $resp->faultCode());
961 85
                    }
962
                } else if ($decodeFault) {
963
                    if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) ||
964
                            (strpos($faultResponse, '%faultString%') !== false))) {
965
                        $faultResponse = str_replace(array('%faultCode%', '%faultString%'),
966
                            array($resp->faultCode(), $resp->faultString()), $faultResponse);
967 85
                    }
968 85
                    return $faultResponse;
969 85
                } else {
970 85
                    return $resp;
971 64
                }
972 64
            } else {
973 64
                return $encoder->decode($resp->value(), $decodeOptions);
974 64
            }
975
        };
976
977 64
        return $function;
978
    }
979
980
    /**
981
     * @internal made public just for Debugger usage
982
     *
983
     * @param Client $client
984
     * @param string $methodName
985 64
     * @param array $extraOptions @see wrapXmlrpcMethod
986 64
     * @param string $newFuncName
987
     * @param array $mSig
988 85
     * @param string $mDesc
989 64
     * @return string[] keys: source, docstring
990 64
     */
991
    public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='')
992 85
    {
993 85
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
994
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
995 85
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
996 85
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
997
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
998
        $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
999
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix;
1000
        $clientReturnType = isset($extraOptions['client_return_type']) ? $extraOptions['client_return_type'] : $prefix;
1001
        $throwFault = false;
1002
        $decodeFault = false;
1003 85
        $faultResponse = null;
1004
        if (isset($extraOptions['throw_on_fault'])) {
1005 85
            $throwFault = $extraOptions['throw_on_fault'];
1006 22
        } else if (isset($extraOptions['return_on_fault'])) {
1007
            $decodeFault = true;
1008 64
            $faultResponse = $extraOptions['return_on_fault'];
1009
        }
1010
1011 85
        $code = "function $newFuncName(";
1012
        if ($clientCopyMode < 2) {
1013 85
            // client copy mode 0 or 1 == full / partial client copy in emitted code
1014
            $verbatimClientCopy = !$clientCopyMode;
1015
            $innerCode = '  ' . str_replace("\n", "\n  ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $clientReturnType, static::$namespace));
1016
            $innerCode .= "\$client->setDebug(\$debug);\n";
1017
            $this_ = '';
1018
        } else {
1019
            // client copy mode 2 == no client copy in emitted code
1020
            $innerCode = '';
1021
            $this_ = 'this->';
1022
        }
1023
        $innerCode .= "  \$req = new " . static::$namespace . "Request('$methodName');\n";
1024
1025
        if ($mDesc != '') {
1026
            // take care that PHP comment is not terminated unwillingly by method description
1027
            /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags...
1028
            $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n";
1029
        } else {
1030
            $mDesc = "/**\n * Function $newFuncName.\n";
1031
        }
1032
1033
        // param parsing
1034 22
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
1035
        $plist = array();
1036 22
        $pCount = count($mSig);
1037 22
        for ($i = 1; $i < $pCount; $i++) {
1038 22
            $plist[] = "\$p$i";
1039 22
            $pType = $mSig[$i];
1040 22
            if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
1041 22
                $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
1042 22
            ) {
1043 22
                // only build directly xml-rpc values when type is known and scalar
1044 22
                $innerCode .= "  \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n";
1045 22
            } else {
1046
                if ($encodePhpObjects || $encodeNulls) {
1047 22
                    $encOpts = array();
1048 22
                    if ($encodePhpObjects) {
1049
                        $encOpts[] = 'encode_php_objs';
1050 22
                    }
1051 22
                    if ($encodeNulls) {
1052 22
                        $encOpts[] = 'null_extension';
1053
                    }
1054
1055
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n";
1056
                } else {
1057 22
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i);\n";
1058 22
                }
1059 22
            }
1060 22
            $innerCode .= "  \$req->addParam(\$p$i);\n";
1061
            $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n";
1062 22
        }
1063
        if ($clientCopyMode < 2) {
1064
            $plist[] = '$debug = 0';
1065
            $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n";
1066
        }
1067
        $plist = implode(', ', $plist);
1068 22
        $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]);
1069
        if ($throwFault) {
1070
            $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\PhpXmlRpc\\Exception');
1071 22
        } else if ($decodeFault) {
1072 22
            $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)";
1073
        } else {
1074 22
            $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)";
1075 21
        }
1076
        $mDesc .= "\n */\n";
1077
1078
        /// @todo move setting of timeout, protocol to outside the send() call
1079 22
        $innerCode .= "  \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
1080 22
        if ($throwFault) {
1081 22
            if (!is_string($throwFault)) {
1082 22
                $throwFault = '\\PhpXmlRpc\\Exception';
1083
            }
1084 22
            $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())";
1085 22
        } else if ($decodeFault) {
1086 22
            if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
1087 22
                $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
1088 22
            } else {
1089 22
                $respCode = 'return ' . var_export($faultResponse, true);
1090 22
            }
1091
        } else {
1092
            $respCode = 'return $res';
1093 22
        }
1094 22
        if ($decodePhpObjects) {
1095
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
1096 22
        } else {
1097 22
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());";
1098 22
        }
1099 22
1100 22
        $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n";
1101
1102
        return array('source' => $code, 'docstring' => $mDesc);
1103 22
    }
1104
1105
    /**
1106
     * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote
1107
     * server as own methods.
1108
     * For a slimmer alternative, see the code in demo/client/proxy.php.
1109 22
     * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but
1110 22
     * we do not support them yet...
1111 22
     *
1112 22
     * @see wrapXmlrpcMethod for more details.
1113 22
     *
1114 22
     * @param Client $client the client obj all set to query the desired server
1115
     * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus
1116
     *                            - string method_filter      regular expression
1117
     *                            - string new_class_name
1118
     *                            - string prefix
1119
     *                            - bool   simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class
1120
     * @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)
1121
     *
1122
     * @todo add support for anonymous classes in the 'buildIt' case for php > 7
1123
     * @todo add method setDebug() to new class, to enable/disable debugging
1124
     * @todo optimization - move the generated Encoder instance to be a property of the created class, instead of creating
1125
     *                      it on every generated method invocation
1126
     */
1127
    public function wrapXmlrpcServer($client, $extraOptions = array())
1128
    {
1129
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
1130
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
1131
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
1132
        $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : '';
1133
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
1134
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
1135
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
1136
        $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true;
1137 85
        $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false;
1138
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
1139 85
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : static::$prefix;
1140 85
        $clientReturnType = isset($extraOptions['client_return_type']) ? $extraOptions['client_return_type'] : $prefix;
1141
1142
        $reqClass = static::$namespace . 'Request';
1143
        $decoderClass = static::$namespace . 'Encoder';
1144 85
1145 85
        // retrieve the list of methods
1146
        $req = new $reqClass('system.listMethods');
1147
        /// @todo move setting of timeout, protocol to outside the send() call
1148
        $response = $client->send($req, $timeout, $protocol);
1149
        if ($response->faultCode()) {
1150
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server');
1151 85
1152 85
            return false;
1153 85
        }
1154
        $mList = $response->value();
1155
        /// @todo what about return_type = xml?
1156
        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
1157
            $decoder = new $decoderClass();
1158 85
            $mList = $decoder->decode($mList);
1159
        }
1160 85
        if (!is_array($mList) || !count($mList)) {
1161
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server');
1162
1163
            return false;
1164
        }
1165
1166
        // pick a suitable name for the new function, avoiding collisions
1167
        if ($newClassName != '') {
1168
            $xmlrpcClassName = $newClassName;
1169
        } else {
1170
            /// @todo direct access to $client->server is now deprecated
1171
            $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), array('_', ''),
1172
                $client->server) . '_client';
1173
        }
1174
        while ($buildIt && class_exists($xmlrpcClassName)) {
1175
            $xmlrpcClassName .= 'x';
1176
        }
1177
1178
        $source = "class $xmlrpcClassName\n{\n  public \$client;\n\n";
1179
        $source .= "  function __construct()\n  {\n";
1180
        $source .= '    ' . str_replace("\n", "\n    ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $clientReturnType, static::$namespace));
1181
        $source .= "\$this->client = \$client;\n  }\n\n";
1182
        $opts = array(
1183
            'return_source' => true,
1184
            'simple_client_copy' => 2, // do not produce code to copy the client object
1185
            'timeout' => $timeout,
1186
            'protocol' => $protocol,
1187
            'encode_nulls' => $encodeNulls,
1188
            'encode_php_objs' => $encodePhpObjects,
1189
            'decode_php_objs' => $decodePhpObjects,
1190
            'throw_on_fault' => $throwOnFault,
1191
            'prefix' => $prefix,
1192
        );
1193
1194
        /// @todo build phpdoc for class definition, too
1195
        foreach ($mList as $mName) {
1196
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
1197
                /// @todo this will fail if server exposes 2 methods called f.e. do.something and do_something
1198
                $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1199
                    array('_', ''), $mName);
1200
                $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts);
1201
                if ($methodWrap) {
1202
                    if ($buildIt) {
1203
                        $source .= $methodWrap['source'] . "\n";
1204
1205
                    } else {
1206
                        $source .= '  ' . str_replace("\n", "\n  ", $methodWrap['docstring']);
1207
                        $source .= str_replace("\n", "\n  ", $methodWrap['source']). "\n";
1208
                    }
1209
1210
                } else {
1211
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName);
1212
                }
1213
            }
1214
        }
1215
        $source .= "}\n";
1216
        if ($buildIt) {
1217
            $allOK = 0;
1218
            eval($source . '$allOK=1;');
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
1219
            if ($allOK) {
1220
                return $xmlrpcClassName;
1221
            } else {
1222
                /// @todo direct access to $client->server is now deprecated
1223
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName .
1224
                    ' to wrap remote server ' . $client->server);
1225
                return false;
1226
            }
1227
        } else {
1228
            return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => '');
1229
        }
1230
    }
1231
1232
    /**
1233
     * Given necessary info, generate php code that will build a client object just like the given one.
1234
     * Take care that no full checking of input parameters is done to ensure that valid php code is emitted.
1235
     * @param Client $client
1236
     * @param bool $verbatimClientCopy when true, copy the whole options of the client, except for 'debug' and 'return_type'
1237
     * @param string $prefix used for the return_type of the created client (postfixed with 'vals')
1238
     * @param string $namespace
1239
     * @return string
1240
     */
1241
    protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\')
1242
    {
1243
        $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->getUrl()) .
1244
            "');\n";
1245
1246
        // copy all client fields to the client that will be generated runtime
1247
        // (this provides for future expansion or subclassing of client obj)
1248
        if ($verbatimClientCopy) {
1249
            foreach ($client->getOptions() as $opt => $val) {
1250
                if ($opt != 'debug' && $opt != 'return_type') {
1251
                    $val = var_export($val, true);
1252
                    $code .= "\$client->setOption('$opt', $val);\n";
1253
                }
1254
            }
1255
        }
1256
        // only make sure that client always returns the correct data type
1257
        $code .= "\$client->setOption(\PhpXmlRpc\Client::OPT_RETURN_TYPE, '{$prefix}vals');\n";
1258
        return $code;
1259
    }
1260
1261
    /**
1262
     * @param string $index use '*' as wildcard for an objet to be used whenever one with the appropriate index is not found
1263
     * @param object $object
1264
     * @return void
1265
     */
1266
    public static function holdObject($index, $object)
1267
    {
1268
        self::$objHolder[$index] = $object;
1269
    }
1270
1271
    /**
1272
     * @param string $index
1273
     * @return object
1274
     * @throws ValueErrorException
1275
     */
1276
    public static function getHeldObject($index)
1277
    {
1278
        if (isset(self::$objHolder[$index])) {
1279
            return self::$objHolder[$index];
1280
        }
1281
        if (isset(self::$objHolder['*'])) {
1282
            return self::$objHolder['*'];
1283
        }
1284
1285
        throw new ValueErrorException("No object held for index '$index'");
1286
    }
1287
1288
    /**
1289
     * @param Client $client
1290
     * @return Client
1291
     */
1292
    protected function cloneClientForClosure($client)
1293
    {
1294
        return clone $client;
1295
    }
1296
}
1297