Passed
Push — master ( f31a58...bb780a )
by Gaetano
09:56
created

Wrapper::getHeldObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 6
rs 10
1
<?php
2
/**
3
 * @author Gaetano Giunta
4
 * @copyright (C) 2006-2023 G. Giunta
5
 * @license code licensed under the BSD License: see file license.txt
6
 */
7
8
namespace PhpXmlRpc;
9
10
use PhpXmlRpc\Helper\Logger;
11
12
/**
13
 * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa.
14
 * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies.
15
 *
16
 * @todo use some better templating system for code generation?
17
 * @todo implement method wrapping with preservation of php objs in calls
18
 * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
19
 * @todo add support for 'epivals' mode
20
 * @todo allow setting custom namespace for generated wrapping code
21
 */
22
class Wrapper
23
{
24
    /**
25
     * @var object[]
26
     * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode
27
     * @internal this property will become protected in the future
28
     */
29 24
    public static $objHolder = array();
30
31 24
    protected static $logger;
32 1
33
    /** @var string */
34 24
    protected static $namespace = '\\PhpXmlRpc\\';
35
36
    public function getLogger()
37
    {
38
        if (self::$logger === null) {
39
            self::$logger = Logger::instance();
40
        }
41
        return self::$logger;
42
    }
43
44
    /**
45
     * @param $logger
46
     * @return void
47
     */
48
    public static function setLogger($logger)
49
    {
50
        self::$logger = $logger;
51
    }
52
53
    /**
54
     * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
55
     * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
56
     * Notes:
57 559
     * - for php 'resource' types returns empty string, since resources cannot be serialized;
58
     * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs
59 559
     * - for php arrays always return array, even though arrays sometimes serialize as structs...
60 559
     * - for 'void' and 'null' returns 'undefined'
61 559
     *
62 559
     * @param string $phpType
63 559
     * @return string
64 559
     *
65 559
     * @todo support notation `something[]` as 'array'
66 559
     * @todo check if nil support is enabled when finding null
67 559
     */
68
    public function php2XmlrpcType($phpType)
69 559
    {
70 559
        switch (strtolower($phpType)) {
71 559
            case 'string':
72 559
                return Value::$xmlrpcString;
73
            case 'integer':
74 559
            case Value::$xmlrpcInt: // 'int'
75 559
            case Value::$xmlrpcI4:
76
            case Value::$xmlrpcI8:
77 559
                return Value::$xmlrpcInt;
78 559
            case Value::$xmlrpcDouble: // 'double'
79
                return Value::$xmlrpcDouble;
80 559
            case 'bool':
81
            case Value::$xmlrpcBoolean: // 'boolean'
82 559
            case 'false':
83
            case 'true':
84
                return Value::$xmlrpcBoolean;
85 559
            case Value::$xmlrpcArray: // 'array':
86 559
            case 'array[]';
87
                return Value::$xmlrpcArray;
88
            case 'object':
89 559
            case Value::$xmlrpcStruct: // 'struct'
90
                return Value::$xmlrpcStruct;
91
            case Value::$xmlrpcBase64:
92 559
                return Value::$xmlrpcBase64;
93
            case 'resource':
94
                return '';
95
            default:
96
                if (class_exists($phpType)) {
97
                    // DateTimeInterface is not present in php 5.4...
98
                    if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) {
99
                        return Value::$xmlrpcDateTime;
100
                    }
101
                    return Value::$xmlrpcStruct;
102
                } else {
103
                    // unknown: might be any 'extended' xml-rpc type
104 85
                    return Value::$xmlrpcValue;
105
                }
106 85
        }
107 85
    }
108 85
109 85
    /**
110 64
     * Given a string defining a phpxmlrpc type return the corresponding php type.
111 85
     *
112 22
     * @param string $xmlrpcType
113 22
     * @return string
114 64
     */
115 22
    public function xmlrpc2PhpType($xmlrpcType)
116 22
    {
117
        switch (strtolower($xmlrpcType)) {
118 22
            case 'base64':
119
            case 'datetime.iso8601':
120 22
            case 'string':
121 22
                return Value::$xmlrpcString;
122
            case 'int':
123
            case 'i4':
124
            case 'i8':
125
                return 'integer';
126
            case 'struct':
127
            case 'array':
128
                return 'array';
129
            case 'double':
130
                return 'float';
131
            case 'undefined':
132
                return 'mixed';
133
            case 'boolean':
134
            case 'null':
135
            default:
136
                // unknown: might be any xml-rpc type
137
                return strtolower($xmlrpcType);
138
        }
139
    }
140
141
    /**
142
     * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an
143
     * xml-rpc server object and called from remote clients (as well as its corresponding signature info).
144
     *
145
     * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the
146
     * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as
147
     * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to
148
     * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by
149
     * the lib, while datetime values are passed around as strings)
150
     *
151
     * Known limitations:
152
     * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving
153
     *   number/type of params for those)
154
     * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of
155
     *   those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php
156
     *   objects will be rebuilt.
157
     *   In short: php objects can be serialized, too (except for their resource members), using this function.
158
     *   Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard
159
     *   attribute on struct element tags)
160
     *
161
     * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions
162
     * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and
163
     * $exception_handling properties.
164
     *
165
     * @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
Bug introduced by
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...
166
     * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true
167
     * @param array $extraOptions (optional) array of options for conversion. valid values include:
168
     *                            - bool return_source     when true, php code w. function definition will be returned, instead of a closure
169
     *                            - bool encode_nulls      let php objects be sent to server using <nil> elements instead of empty strings
170
     *                            - 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
171
     *                            - bool decode_php_objs   --- WARNING !!! possible security hazard. only use it with trusted servers ---
172
     *                            - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked
173
     * @return array|false false on error, or an array containing the name of the new php function,
174
     *                     its signature and docs, to be used in the server dispatch map
175
     *
176
     * @todo decide how to deal with params passed by ref in function definition: bomb out or allow?
177 559
     * @todo finish using phpdoc info to build method sig if all params are named but out of order
178
     * @todo add a check for params of 'resource' type
179 559
     * @todo add some trigger_errors / error_log when returning false?
180
     * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
181 559
     * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
182 559
     * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
183
     * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway
184 559
     */
185 559
    public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array())
186
    {
187
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
188
189 559
        if (is_string($callable) && strpos($callable, '::') !== false) {
0 ignored issues
show
introduced by
The condition is_string($callable) is always false.
Loading history...
190 559
            $callable = explode('::', $callable);
191 559
        }
192 559
        if (is_array($callable)) {
0 ignored issues
show
introduced by
The condition is_array($callable) is always false.
Loading history...
193
            if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) {
194 559
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong');
195 559
                return false;
196
            }
197 559
            if (is_string($callable[0])) {
198
                $plainFuncName = implode('::', $callable);
199
            } elseif (is_object($callable[0])) {
200
                $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
201
            }
202 559
            $exists = method_exists($callable[0], $callable[1]);
203 559
        } else if ($callable instanceof \Closure) {
204
            // we do not support creating code which wraps closures, as php does not allow to serialize them
205 559
            if (!$buildIt) {
206 559
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code');
207
                return false;
208
            }
209 559
210
            $plainFuncName = 'Closure';
211
            $exists = true;
212
        } else {
213
            $plainFuncName = $callable;
214 559
            $exists = function_exists($callable);
215 559
        }
216
217
        if (!$exists) {
218
            $this->getLogger()->errorLog('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...
219 559
            return false;
220
        }
221 559
222 559
        $funcDesc = $this->introspectFunction($callable, $plainFuncName);
223
        if (!$funcDesc) {
224 559
            return false;
225 559
        }
226
227
        $funcSigs = $this->buildMethodSignatures($funcDesc);
228
229 559
        if ($buildIt) {
230 559
            $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc);
231 559
        } else {
232 559
            $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions);
233
            $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc);
234 559
        }
235 559
236 559
        $ret = array(
237
            'function' => $callable,
238 559
            'signature' => $funcSigs['sigs'],
239
            'docstring' => $funcDesc['desc'],
240
            'signature_docs' => $funcSigs['sigsDocs'],
241
        );
242
        if (!$buildIt) {
243
            $ret['function'] = $newFuncName;
244
            $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...
245
        }
246
        return $ret;
247
    }
248 559
249
    /**
250
     * Introspect a php callable and its phpdoc block and extract information about its signature
251 559
     *
252 559
     * @param callable $callable
253 559
     * @param string $plainFuncName
254
     * @return array|false
255
     */
256
    protected function introspectFunction($callable, $plainFuncName)
257 559
    {
258
        // start to introspect PHP code
259
        if (is_array($callable)) {
260
            $func = new \ReflectionMethod($callable[0], $callable[1]);
261 559
            if ($func->isPrivate()) {
262
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
263
                return false;
264
            }
265 559
            if ($func->isProtected()) {
266
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
267
                return false;
268
            }
269 559
            if ($func->isConstructor()) {
270
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
271
                return false;
272
            }
273
            if ($func->isDestructor()) {
274
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
275 559
                return false;
276
            }
277 559
            if ($func->isAbstract()) {
278
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName);
279
                return false;
280
            }
281
            /// @todo add more checks for static vs. nonstatic?
282
        } else {
283
            $func = new \ReflectionFunction($callable);
284
        }
285
        if ($func->isInternal()) {
286
            /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully
287 559
            ///       reflect internal php functions
288
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName);
289 559
            return false;
290
        }
291 559
292
        // retrieve parameter names, types and description from javadoc comments
293 559
294
        // function description
295 559
        $desc = '';
296 559
        // type of return val: by default 'any'
297 559
        $returns = Value::$xmlrpcValue;
298 559
        // desc of return val
299 559
        $returnsDocs = '';
300 559
        // type + name of function parameters
301 559
        $paramDocs = array();
302 559
303 559
        $docs = $func->getDocComment();
304
        if ($docs != '') {
305 559
            $docs = explode("\n", $docs);
306 559
            $i = 0;
307
            foreach ($docs as $doc) {
308 559
                $doc = trim($doc, " \r\t/*");
309 559
                if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
310
                    if ($desc) {
311 559
                        $desc .= "\n";
312 559
                    }
313
                    $desc .= $doc;
314 559
                } elseif (strpos($doc, '@param') === 0) {
315 559
                    // syntax: @param type $name [desc]
316
                    if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) {
317 559
                        $name = strtolower(trim($matches[2]));
318 559
                        //$paramDocs[$name]['name'] = trim($matches[2]);
319 559
                        $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : '';
320 559
                        $paramDocs[$name]['type'] = $matches[1];
321
                    }
322
                    $i++;
323
                } elseif (strpos($doc, '@return') === 0) {
324
                    // syntax: @return type [desc]
325
                    if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
326
                        $returns = $matches[1];
327
                        if (isset($matches[2])) {
328 559
                            $returnsDocs = trim($matches[2]);
329 559
                        }
330 559
                    }
331 559
                }
332 559
            }
333 559
        }
334 559
335
        // execute introspection of actual function prototype
336
        $params = array();
337
        $i = 0;
338 559
        foreach ($func->getParameters() as $paramObj) {
339 559
            $params[$i] = array();
340 559
            $params[$i]['name'] = '$' . $paramObj->getName();
341 559
            $params[$i]['isoptional'] = $paramObj->isOptional();
342 559
            $i++;
343 559
        }
344
345
        return array(
346
            'desc' => $desc,
347
            'docs' => $docs,
348
            'params' => $params, // array, positionally indexed
349
            'paramDocs' => $paramDocs, // array, indexed by name
350
            'returns' => $returns,
351
            'returnsDocs' =>$returnsDocs,
352
        );
353
    }
354
355
    /**
356
     * Given the method description given by introspection, create method signature data
357 559
     *
358
     * @param array $funcDesc as generated by self::introspectFunction()
359 559
     * @return array
360 559
     *
361 559
     * @todo support better docs with multiple types separated by pipes by creating multiple signatures
362 559
     *       (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences)
363 559
     */
364
    protected function buildMethodSignatures($funcDesc)
365
    {
366
        $i = 0;
367
        $parsVariations = array();
368
        $pars = array();
369
        $pNum = count($funcDesc['params']);
370
        foreach ($funcDesc['params'] as $param) {
371
            /* // match by name real param and documented params
372
            $name = strtolower($param['name']);
373 559
            if (!isset($funcDesc['paramDocs'][$name])) {
374
                $funcDesc['paramDocs'][$name] = array();
375
            }
376
            if (!isset($funcDesc['paramDocs'][$name]['type'])) {
377
                $funcDesc['paramDocs'][$name]['type'] = 'mixed';
378 559
            }*/
379 559
380 559
            if ($param['isoptional']) {
381
                // this particular parameter is optional. save as valid previous list of parameters
382 559
                $parsVariations[] = $pars;
383
            }
384
385
            $pars[] = "\$p$i";
386 559
            $i++;
387
            if ($i == $pNum) {
388 559
                // last allowed parameters combination
389
                $parsVariations[] = $pars;
390
            }
391 559
        }
392 559
393 559
        if (count($parsVariations) == 0) {
394
            // only known good synopsis = no parameters
395 559
            $parsVariations[] = array();
396 559
        }
397 559
398 559
        $sigs = array();
399 559
        $sigsDocs = array();
400 559
        foreach ($parsVariations as $pars) {
401
            // build a signature
402 559
            $sig = array($this->php2XmlrpcType($funcDesc['returns']));
403
            $pSig = array($funcDesc['returnsDocs']);
404 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...
405
                $name = strtolower($funcDesc['params'][$i]['name']);
406 559
                if (isset($funcDesc['paramDocs'][$name]['type'])) {
407 559
                    $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']);
408
                } else {
409
                    $sig[] = Value::$xmlrpcValue;
410
                }
411 559
                $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : '';
412 559
            }
413
            $sigs[] = $sig;
414
            $sigsDocs[] = $pSig;
415
        }
416
417
        return array(
418
            'sigs' => $sigs,
419
            'sigsDocs' => $sigsDocs
420
        );
421
    }
422
423
    /**
424
     * Creates a closure that will execute $callable
425
     *
426
     * @param $callable
427 559
     * @param array $extraOptions
428
     * @param string $plainFuncName
429
     * @param array $funcDesc
430
     * @return \Closure
431
     *
432
     * @todo validate params? In theory all validation is left to the dispatch map...
433
     * @todo add support for $catchWarnings
434
     */
435 108
    protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
436 108
    {
437 108
        /**
438 108
         * @param Request $req
439
         *
440
         * @return mixed
441
         */
442 108
        $function = function($req) use($callable, $extraOptions, $funcDesc)
443 108
        {
444 108
            $encoderClass = static::$namespace.'Encoder';
445 108
            $responseClass = static::$namespace.'Response';
446
            $valueClass = static::$namespace.'Value';
447
448
            // validate number of parameters received
449
            // this should be optional really, as we assume the server does the validation
450
            $minPars = count($funcDesc['params']);
451 108
            $maxPars = $minPars;
452 108
            foreach ($funcDesc['params'] as $i => $param) {
453
                if ($param['isoptional']) {
454
                    // this particular parameter is optional. We assume later ones are as well
455
                    $minPars = $i;
456 108
                    break;
457 108
                }
458 108
            }
459
            $numPars = $req->getNumParams();
460
            if ($numPars < $minPars || $numPars > $maxPars) {
461 108
                return new $responseClass(0, 3, 'Incorrect parameters passed to method');
462
            }
463 108
464
            $encoder = new $encoderClass();
465 108
            $options = array();
466 108
            if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) {
467
                $options[] = 'decode_php_objs';
468
            }
469 108
            $params = $encoder->decode($req, $options);
470 108
471 1
            $result = call_user_func_array($callable, $params);
472
473
            if (! is_a($result, $responseClass)) {
474 108
                // q: why not do the same for int, float, bool, string?
475
                if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
476 108
                    $result = new $valueClass($result, $funcDesc['returns']);
477
                } else {
478
                    $options = array();
479 108
                    if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) {
480 559
                        $options[] = 'encode_php_objs';
481
                    }
482 559
                    if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) {
483
                        $options[] = 'null_extension';
484
                    }
485
486
                    $result = $encoder->encode($result, $options);
487
                }
488
                $result = new $responseClass($result);
489
            }
490
491 643
            return $result;
492
        };
493
494
        return $function;
495 643
    }
496
497 643
    /**
498 622
     * Return a name for a new function, based on $callable, insuring its uniqueness
499 559
     * @param mixed $callable a php callable, or the name of an xml-rpc method
500 559
     * @param string $newFuncName when not empty, it is used instead of the calculated version
501
     * @return string
502 559
     */
503
    protected function newFunctionName($callable, $newFuncName, $extraOptions)
504
    {
505 622
        // determine name of new php function
506
507
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
508 622
509 622
        if ($newFuncName == '') {
510 622
            if (is_array($callable)) {
511
                if (is_string($callable[0])) {
512
                    $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable);
513
                } else {
514 22
                    $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1];
515
                }
516
            } else {
517 643
                if ($callable instanceof \Closure) {
518 620
                    $xmlrpcFuncName = "{$prefix}_closure";
519
                } else {
520
                    $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
521 643
                        array('_', ''), $callable);
522
                    $xmlrpcFuncName = "{$prefix}_$callable";
523
                }
524
            }
525
        } else {
526
            $xmlrpcFuncName = $newFuncName;
527
        }
528
529
        while (function_exists($xmlrpcFuncName)) {
530
            $xmlrpcFuncName .= 'x';
531
        }
532
533
        return $xmlrpcFuncName;
534 559
    }
535
536 559
    /**
537
     * @param $callable
538 559
     * @param string $newFuncName
539 559
     * @param array $extraOptions
540 559
     * @param string $plainFuncName
541
     * @param array $funcDesc
542 559
     * @return string
543 559
     */
544 559
    protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
545 559
    {
546 559
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
547
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
548 559
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
549
        $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : '';
550
551
        $i = 0;
552
        $parsVariations = array();
553 559
        $pars = array();
554 559
        $pNum = count($funcDesc['params']);
555 559
        foreach ($funcDesc['params'] as $param) {
556
557 559
            if ($param['isoptional']) {
558
                // this particular parameter is optional. save as valid previous list of parameters
559
                $parsVariations[] = $pars;
560
            }
561 559
562
            $pars[] = "\$params[$i]";
563
            $i++;
564
            if ($i == $pNum) {
565
                // last allowed parameters combination
566
                $parsVariations[] = $pars;
567 559
            }
568 559
        }
569
570
        if (count($parsVariations) == 0) {
571
            // only known good synopsis = no parameters
572
            $parsVariations[] = array();
573 559
            $minPars = 0;
574 559
            $maxPars = 0;
575
        } else {
576 559
            $minPars = count($parsVariations[0]);
577 559
            $maxPars = count($parsVariations[count($parsVariations)-1]);
578
        }
579
580 559
        // build body of new function
581
582
        $innerCode = "  \$paramCount = \$req->getNumParams();\n";
583
        $innerCode .= "  if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n";
584
585 559
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
586 559
        if ($decodePhpObjects) {
587 559
            $innerCode .= "  \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n";
588 559
        } else {
589
            $innerCode .= "  \$params = \$encoder->decode(\$req);\n";
590 559
        }
591
592 559
        // since we are building source code for later use, if we are given an object instance,
593 559
        // we go out of our way and store a pointer to it in a static class var...
594 559
        if (is_array($callable) && is_object($callable[0])) {
595
            static::holdObject($newFuncName, $callable[0]);
596
            $class = get_class($callable[0]);
597 559
            if ($class[0] !== '\\') {
598 559
                $class = '\\' . $class;
599
            }
600
            $innerCode .= "  /// @var $class \$obj\n";
601 559
            $innerCode .= "  \$obj = PhpXmlRpc\\Wrapper::getHeldObject('$newFuncName');\n";
602
            $realFuncName = '$obj->' . $callable[1];
603
        } else {
604 559
            $realFuncName = $plainFuncName;
605
        }
606
        foreach ($parsVariations as $i => $pars) {
607
            $innerCode .= "  if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n";
608
            if ($i < (count($parsVariations) - 1))
609
                $innerCode .= "  else\n";
610
        }
611 559
        $innerCode .= "  if (is_a(\$retVal, '" . static::$namespace . "Response'))\n    return \$retVal;\n  else\n";
612
        /// q: why not do the same for int, float, bool, string?
613 559
        if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
614
            $innerCode .= "    return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));";
615
        } else {
616
            $encodeOptions = array();
617
            if ($encodeNulls) {
618
                $encodeOptions[] = 'null_extension';
619
            }
620
            if ($encodePhpObjects) {
621
                $encodeOptions[] = 'encode_php_objs';
622
            }
623
624
            if ($encodeOptions) {
625
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" .
626
                    implode("', '", $encodeOptions) . "')));";
627
            } else {
628
                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));";
629 559
            }
630
        }
631 559
        // shall we exclude functions returning by ref?
632 559
        // if ($func->returnsReference())
633
        //     return false;
634 559
635 559
        $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" .
636 559
            "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}";
637 559
638 559
        return $code;
639 559
    }
640 559
641 559
    /**
642
     * Given a user-defined PHP class or php object, map its methods onto a list of
643 559
     * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server
644
     * object and called from remote clients (as well as their corresponding signature info).
645 559
     *
646 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
647
     * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus
648
     *                            - 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
649
     *                            - string method_filter  a regexp used to filter methods to wrap based on their names
650
     *                            - string prefix         used for the names of the xml-rpc methods created.
651
     *                            - 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
652
     * @return array|false false on failure, or on array useable for the dispatch map
653 559
     */
654
    public function wrapPhpClass($className, $extraOptions = array())
655
    {
656
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
657
        $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
658
659
        $results = array();
660
        $mList = get_class_methods($className);
661
        foreach ($mList as $mName) {
662 559
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
663
                $func = new \ReflectionMethod($className, $mName);
664 559
                if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
665 559
                    if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($func->isStatic() && $m...& is_object($className), Probably Intended Meaning: $func->isStatic() && ($m... is_object($className))
Loading history...
666
                        (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
667
                    ) {
668 559
                        $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions);
0 ignored issues
show
Bug introduced by
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

668
                        $methodWrap = $this->wrapPhpFunction(/** @scrutinizer ignore-type */ array($className, $mName), '', $extraOptions);
Loading history...
669 559
670
                        if ($methodWrap) {
671
                            $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap;
672
                        }
673 559
                    }
674
                }
675
            }
676
        }
677
678
        return $results;
679
    }
680
681
    /**
682
     * @param string|object $className
683
     * @param string $classMethod
684
     * @param array $extraOptions
685
     * @return string
686
     */
687
    protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array())
688
    {
689
        if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) {
690
            return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . $classMethod;
691
        }
692
693
        if (is_object($className)) {
694
            $realClassName = get_class($className);
695
        } else {
696
            $realClassName = $className;
697
        }
698
        return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . "$realClassName.$classMethod";
699
    }
700
701
    /**
702
     * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results
703
     * using native php types for both arguments and results. The generated php function will return a Response
704
     * object for failed xml-rpc calls.
705
     *
706
     * Known limitations:
707
     * - server must support system.methodSignature for the target xml-rpc method
708
     * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures
709
     *   differ only by number of params and not by type, but it would be more complication than we can spare time for)
710
     * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to
711
     *   the php function if these are structs or arrays whose (sub)members include values of type base64
712
     *
713
     * Notes: the connection properties of the given client will be copied and reused for the connection used during
714
     * the call to the generated php function.
715
     * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation
716
     * and an xmlrpc-connection opened+closed.
717
     * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful
718
     * for debugging purposes.
719
     *
720
     * @param Client $client an xml-rpc client set up correctly to communicate with target server
721 109
     * @param string $methodName the xml-rpc method to be mapped to a php function
722
     * @param array $extraOptions array of options that specify conversion details. Valid options include
723 109
     *                            - integer signum              the index of the method signature to use in mapping (if method exposes many sigs)
724
     *                            - integer timeout             timeout (in secs) to be used when executing function/calling remote method
725 109
     *                            - string  protocol            'http' (default), 'http11', 'https', 'h2' or 'h2c'
726
     *                            - string  new_function_name   the name of php function to create, when return_source is used. If unspecified, lib will pick an appropriate name
727 109
     *                            - string  return_source       if true return php code w. function definition instead of function itself (closure)
728 109
     *                            - bool    encode_nulls        if true, use `<nil>` elements instead of empty string xml-rpc values for php null values
729 24
     *                            - 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
730
     *                            - bool    decode_php_objs     --- WARNING !!! possible security hazard. only use it with trusted servers ---
731
     *                            - mixed   return_on_fault     a php value to be returned when the xml-rpc call fails/returns a fault response (by default the Response object is returned in this case). If a string is used, '%faultCode%' and '%faultString%' tokens will be substituted with actual error values
732 86
     *                            - bool    throw_on_fault      if true, throw an exception instead of returning a Response in case of errors/faults;
733 1
     *                                                          if a string, do the same and assume it is the exception class to throw
734
     *                            - bool    debug               set it to 1 or 2 to see debug results of querying server for method synopsis
735
     *                            - int     simple_client_copy  set it to 1 to have a lightweight copy of the $client object made in the generated code (only used when return_source = true)
736
     * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true
737 85
     *
738
     * @todo allow the created function to throw exceptions on method calls failures
739 85
     * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it'
740
     * @todo if we can not retrieve method signature, create a php function with varargs
741 85
     * @todo if caller did not specify a specific sig, shall we support all of them?
742
     *       It might be hard (hence slow) to match based on type and number of arguments...
743
     */
744
    public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array())
745
    {
746
        $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : '';
747 85
748
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
749 85
750
        $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions);
751
        if (!$mSig) {
752
            return false;
753
        }
754
755
        if ($buildIt) {
756
            return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig);
757
        } else {
758
            // if in 'offline' mode, retrieve method description too.
759
            // in online mode, favour speed of operation
760 109
            $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions);
761
762 109
            $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions);
763 109
764 109
            $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc);
765 109
766
            $results['function'] = $newFuncName;
767 109
768 109
            return $results;
769 109
        }
770 109
    }
771
772 109
    /**
773 109
     * Retrieves an xml-rpc method signature from a server which supports system.methodSignature
774 109
     * @param Client $client
775 109
     * @param string $methodName
776 109
     * @param array $extraOptions
777 24
     * @return false|array
778 24
     */
779
    protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array())
780
    {
781 86
        $reqClass = static::$namespace . 'Request';
782 86
        $valClass = static::$namespace . 'Value';
783 85
        $decoderClass = static::$namespace . 'Encoder';
784 85
785
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
786
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
787 86
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
788
        $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0;
789
790
        $req = new $reqClass('system.methodSignature');
791
        $req->addparam(new $valClass($methodName));
792 86
        $client->setDebug($debug);
793
        $response = $client->send($req, $timeout, $protocol);
794
        if ($response->faultCode()) {
795
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName);
796
            return false;
797
        }
798
799
        $mSig = $response->value();
800
        /// @todo what about return xml?
801 85
        if ($client->return_type != 'phpvals') {
802
            $decoder = new $decoderClass();
803 85
            $mSig = $decoder->decode($mSig);
804 85
        }
805 85
806
        if (!is_array($mSig) || count($mSig) <= $sigNum) {
807 85
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName);
808 85
            return false;
809 85
        }
810
811 85
        return $mSig[$sigNum];
812
    }
813 85
814 85
    /**
815 85
     * @param Client $client
816 85
     * @param string $methodName
817 85
     * @param array $extraOptions
818 85
     * @return string in case of any error, an empty string is returned, no warnings generated
819 85
     */
820 85
    protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array())
821
    {
822
        $reqClass = static::$namespace . 'Request';
823
        $valClass = static::$namespace . 'Value';
824 85
825
        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
826
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
827
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
828
829
        $mDesc = '';
830
831
        $req = new $reqClass('system.methodHelp');
832
        $req->addparam(new $valClass($methodName));
833
        $client->setDebug($debug);
834
        $response = $client->send($req, $timeout, $protocol);
835
        if (!$response->faultCode()) {
836 1
            $mDesc = $response->value();
837
            if ($client->return_type != 'phpvals') {
838
                $mDesc = $mDesc->scalarval();
0 ignored issues
show
Bug introduced by
The method scalarval() does not exist on integer. ( Ignorable by Annotation )

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

838
                /** @scrutinizer ignore-call */ 
839
                $mDesc = $mDesc->scalarval();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
839 1
            }
840
        }
841
842 1
        return $mDesc;
843 1
    }
844 1
845 1
    /**
846 1
     * @param Client $client
847
     * @param string $methodName
848
     * @param array $extraOptions @see wrapXmlrpcMethod
849
     * @param array $mSig
850 1
     * @return \Closure
851
     *
852
     * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case?
853 1
     */
854 1
    protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig)
855 1
    {
856 1
        // we clone the client, so that we can modify it a bit independently of the original
857
        $clientClone = clone $client;
858 1
        $function = function() use($clientClone, $methodName, $extraOptions, $mSig)
859 1
        {
860 1
            $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
861
            $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
862
            $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
863 1
            $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
864 1
            $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
865
            $throwFault = false;
866
            $decodeFault = false;
867
            $faultResponse = null;
868
            if (isset($extraOptions['throw_on_fault'])) {
869
                $throwFault = $extraOptions['throw_on_fault'];
870
            } else if (isset($extraOptions['return_on_fault'])) {
871 1
                $decodeFault = true;
872 1
                $faultResponse = $extraOptions['return_on_fault'];
873 1
            }
874 1
875 1
            $reqClass = static::$namespace . 'Request';
876
            $encoderClass = static::$namespace . 'Encoder';
877
            $valueClass = static::$namespace . 'Value';
878 1
879 1
            $encoder = new $encoderClass();
880 1
            $encodeOptions = array();
881
            if ($encodePhpObjects) {
882
                $encodeOptions[] = 'encode_php_objs';
883 1
            }
884 1
            if ($encodeNulls) {
885
                $encodeOptions[] = 'null_extension';
886
            }
887
            $decodeOptions = array();
888
            if ($decodePhpObjects) {
889 1
                $decodeOptions[] = 'decode_php_objs';
890
            }
891
892
            /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not...
893
894
            // support one extra parameter: debug
895 1
            $maxArgs = count($mSig)-1; // 1st element is the return type
896
            $currentArgs = func_get_args();
897 1
            if (func_num_args() == ($maxArgs+1)) {
898 1
                $debug = array_pop($currentArgs);
899 1
                $clientClone->setDebug($debug);
900
            }
901
902
            $xmlrpcArgs = array();
903
            foreach ($currentArgs as $i => $arg) {
904
                if ($i == $maxArgs) {
905
                    break;
906
                }
907
                $pType = $mSig[$i+1];
908
                if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
909
                    $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
910
                ) {
911 1
                    // by building directly xml-rpc values when type is known and scalar (instead of encode() calls),
912
                    // we make sure to honour the xml-rpc signature
913 1
                    $xmlrpcArgs[] = new $valueClass($arg, $pType);
914
                } else {
915 1
                    $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions);
916
                }
917
            }
918
919
            $req = new $reqClass($methodName, $xmlrpcArgs);
920
            // use this to get the maximum decoding flexibility
921
            $clientClone->return_type = 'xmlrpcvals';
922
            $resp = $clientClone->send($req, $timeout, $protocol);
923
            if ($resp->faultcode()) {
924
                if ($throwFault) {
925
                    if (is_string($throwFault)) {
926
                        throw new $throwFault($resp->faultString(), $resp->faultCode());
927 85
                    } else {
928
                        throw new \Exception($resp->faultString(), $resp->faultCode());
929 85
                    }
930 85
                } else if ($decodeFault) {
931 85
                    if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) ||
932 85
                            (strpos($faultResponse, '%faultString%') !== false))) {
933 85
                        $faultResponse = str_replace(array('%faultCode%', '%faultString%'),
934 85
                            array($resp->faultCode(), $resp->faultString()), $faultResponse);
935 85
                    }
936
                    return $faultResponse;
937
                } else {
938
                    return $resp;
939 85
                }
940 85
            } else {
941
                return $encoder->decode($resp->value(), $decodeOptions);
942
            }
943 85
        };
944
945 85
        return $function;
946 85
    }
947
948 64
    /**
949 64
     * @internal made public just for Debugger usage
950 64
     *
951 64
     * @param Client $client
952
     * @param string $methodName
953
     * @param array $extraOptions @see wrapXmlrpcMethod
954 22
     * @param string $newFuncName
955 22
     * @param array $mSig
956
     * @param string $mDesc
957 85
     * @return string[] keys: source, docstring
958
     */
959 85
    public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='')
960
    {
961 85
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
962
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
963
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
964
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
965
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
966
        $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
967 85
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
968 85
        $throwFault = false;
969 85
        $decodeFault = false;
970 85
        $faultResponse = null;
971 64
        if (isset($extraOptions['throw_on_fault'])) {
972 64
            $throwFault = $extraOptions['throw_on_fault'];
973 64
        } else if (isset($extraOptions['return_on_fault'])) {
974 64
            $decodeFault = true;
975
            $faultResponse = $extraOptions['return_on_fault'];
976
        }
977 64
978
        $code = "function $newFuncName(";
979
        if ($clientCopyMode < 2) {
980
            // client copy mode 0 or 1 == full / partial client copy in emitted code
981
            $verbatimClientCopy = !$clientCopyMode;
982
            $innerCode = '  ' . str_replace("\n", "\n  ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
983
            $innerCode .= "\$client->setDebug(\$debug);\n";
984
            $this_ = '';
985 64
        } else {
986 64
            // client copy mode 2 == no client copy in emitted code
987
            $innerCode = '';
988 85
            $this_ = 'this->';
989 64
        }
990 64
        $innerCode .= "  \$req = new " . static::$namespace . "Request('$methodName');\n";
991
992 85
        if ($mDesc != '') {
993 85
            // take care that PHP comment is not terminated unwillingly by method description
994
            /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags...
995 85
            $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n";
996 85
        } else {
997
            $mDesc = "/**\n * Function $newFuncName.\n";
998
        }
999
1000
        // param parsing
1001
        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
1002
        $plist = array();
1003 85
        $pCount = count($mSig);
1004
        for ($i = 1; $i < $pCount; $i++) {
1005 85
            $plist[] = "\$p$i";
1006 22
            $pType = $mSig[$i];
1007
            if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
1008 64
                $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
1009
            ) {
1010
                // only build directly xml-rpc values when type is known and scalar
1011 85
                $innerCode .= "  \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n";
1012
            } else {
1013 85
                if ($encodePhpObjects || $encodeNulls) {
1014
                    $encOpts = array();
1015
                    if ($encodePhpObjects) {
1016
                        $encOpts[] = 'encode_php_objs';
1017
                    }
1018
                    if ($encodeNulls) {
1019
                        $encOpts[] = 'null_extension';
1020
                    }
1021
1022
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n";
1023
                } else {
1024
                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i);\n";
1025
                }
1026
            }
1027
            $innerCode .= "  \$req->addparam(\$p$i);\n";
1028
            $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n";
1029
        }
1030
        if ($clientCopyMode < 2) {
1031
            $plist[] = '$debug = 0';
1032
            $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n";
1033
        }
1034 22
        $plist = implode(', ', $plist);
1035
        $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]);
1036 22
        if ($throwFault) {
1037 22
            $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\Exception');
1038 22
        } else if ($decodeFault) {
1039 22
            $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)";
1040 22
        } else {
1041 22
            $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)";
1042 22
        }
1043 22
        $mDesc .= "\n */\n";
1044 22
1045 22
        $innerCode .= "  \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
1046
        if ($throwFault) {
1047 22
            if (!is_string($throwFault)) {
1048 22
                $throwFault = '\\Exception';
1049
            }
1050 22
            $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())";
1051 22
        } else if ($decodeFault) {
1052 22
            if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
1053
                $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
1054
            } else {
1055
                $respCode = 'return ' . var_export($faultResponse, true);
1056
            }
1057 22
        } else {
1058 22
            $respCode = 'return $res';
1059 22
        }
1060 22
        if ($decodePhpObjects) {
1061
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
1062 22
        } else {
1063
            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());";
1064
        }
1065
1066
        $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n";
1067
1068 22
        return array('source' => $code, 'docstring' => $mDesc);
1069
    }
1070
1071 22
    /**
1072 22
     * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote
1073
     * server as own methods.
1074 22
     * For a slimmer alternative, see the code in demo/client/proxy.php.
1075 21
     * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but
1076
     * we do not support them yet...
1077
     *
1078
     * @see wrapXmlrpcMethod for more details.
1079 22
     *
1080 22
     * @param Client $client the client obj all set to query the desired server
1081 22
     * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus
1082 22
     *                            - string method_filter      regular expression
1083
     *                            - string new_class_name
1084 22
     *                            - string prefix
1085 22
     *                            - bool   simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class
1086 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)
1087 22
     *
1088 22
     * @todo add support for anonymous classes in the 'buildIt' case for php > 7
1089 22
     */
1090 22
    public function wrapXmlrpcServer($client, $extraOptions = array())
1091
    {
1092
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
1093 22
        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
1094 22
        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
1095
        $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : '';
1096 22
        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
1097 22
        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
1098 22
        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
1099 22
        $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true;
1100 22
        $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false;
1101
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
1102
        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
1103 22
1104
        $reqClass = static::$namespace . 'Request';
1105
        $decoderClass = static::$namespace . 'Encoder';
1106
1107
        // retrieve the list of methods
1108
        $req = new $reqClass('system.listMethods');
1109 22
        $response = $client->send($req, $timeout, $protocol);
1110 22
        if ($response->faultCode()) {
1111 22
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server');
1112 22
1113 22
            return false;
1114 22
        }
1115
        $mList = $response->value();
1116
        /// @todo what about return_type = xml?
1117
        if ($client->return_type != 'phpvals') {
1118
            $decoder = new $decoderClass();
1119
            $mList = $decoder->decode($mList);
1120
        }
1121
        if (!is_array($mList) || !count($mList)) {
1122
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server');
1123
1124
            return false;
1125
        }
1126
1127
        // pick a suitable name for the new function, avoiding collisions
1128
        if ($newClassName != '') {
1129
            $xmlrpcClassName = $newClassName;
1130
        } else {
1131
            $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1132
                    array('_', ''), $client->server) . '_client';
1133
        }
1134
        while ($buildIt && class_exists($xmlrpcClassName)) {
1135
            $xmlrpcClassName .= 'x';
1136
        }
1137 85
1138
        /// @todo add method setDebug() to new class, to enable/disable debugging
1139 85
        $source = "class $xmlrpcClassName\n{\n  public \$client;\n\n";
1140 85
        $source .= "  function __construct()\n  {\n";
1141
        $source .= '    ' . str_replace("\n", "\n    ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
1142
        $source .= "\$this->client = \$client;\n  }\n\n";
1143
        $opts = array(
1144 85
            'return_source' => true,
1145 85
            'simple_client_copy' => 2, // do not produce code to copy the client object
1146
            'timeout' => $timeout,
1147
            'protocol' => $protocol,
1148
            'encode_nulls' => $encodeNulls,
1149
            'encode_php_objs' => $encodePhpObjects,
1150
            'decode_php_objs' => $decodePhpObjects,
1151 85
            'throw_on_fault' => $throwOnFault,
1152 85
            'prefix' => $prefix,
1153 85
        );
1154
1155
        /// @todo build phpdoc for class definition, too
1156
        foreach ($mList as $mName) {
1157
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
1158 85
                // note: this will fail if server exposes 2 methods called f.e. do.something and do_something
1159
                $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
1160 85
                    array('_', ''), $mName);
1161
                $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts);
1162
                if ($methodWrap) {
1163
                    if ($buildIt) {
1164
                        $source .= $methodWrap['source'] . "\n";
1165
1166
                    } else {
1167
                        $source .= '  ' . str_replace("\n", "\n  ", $methodWrap['docstring']);
1168
                        $source .= str_replace("\n", "\n  ", $methodWrap['source']). "\n";
1169
                    }
1170
1171
                } else {
1172
                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName);
1173
                }
1174
            }
1175
        }
1176
        $source .= "}\n";
1177
        if ($buildIt) {
1178
            $allOK = 0;
1179
            eval($source . '$allOK=1;');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
1180
            if ($allOK) {
1181
                return $xmlrpcClassName;
1182
            } else {
1183
                $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName . ' to wrap remote server ' . $client->server);
1184
                return false;
1185
            }
1186
        } else {
1187
            return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => '');
1188
        }
1189
    }
1190
1191
    /**
1192
     * Given necessary info, generate php code that will build a client object just like the given one.
1193
     * Take care that no full checking of input parameters is done to ensure that valid php code is emitted.
1194
     * @param Client $client
1195
     * @param bool $verbatimClientCopy when true, copy the whole state of the client, except for 'debug' and 'return_type'
1196
     * @param string $prefix used for the return_type of the created client
1197
     * @param string $namespace
1198
     * @return string
1199
     */
1200
    protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\')
1201
    {
1202
        $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->path) .
1203
            "', '" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->server) . "', $client->port);\n";
1204
1205
        // copy all client fields to the client that will be generated runtime
1206
        // (this provides for future expansion or subclassing of client obj)
1207
        if ($verbatimClientCopy) {
1208
            foreach ($client as $fld => $val) {
1209
                /// @todo in php 8.0, curl handles became objects, but they have no __set_state, thus var_export will
1210
                ///        fail for xmlrpc_curl_handle. So we disabled copying it.
1211
                ///        We should examine in depth if this change can have side effects - at first sight if the
1212
                ///        client's curl handle is not set, all curl options are (re)set on each http call, so there
1213
                ///        should be no loss of state...
1214
                if ($fld != 'debug' && $fld != 'return_type' && $fld != 'xmlrpc_curl_handle') {
1215
                    $val = var_export($val, true);
1216
                    $code .= "\$client->$fld = $val;\n";
1217
                }
1218
            }
1219
        }
1220
        // only make sure that client always returns the correct data type
1221
        $code .= "\$client->return_type = '{$prefix}vals';\n";
1222
        //$code .= "\$client->setDebug(\$debug);\n";
1223
        return $code;
1224
    }
1225
1226
    /**
1227
     * @param string $index
1228
     * @param object $object
1229
     * @return void
1230
     */
1231
    public static function holdObject($index, $object)
1232
    {
1233
        self::$objHolder[$index] = $object;
1234
    }
1235
1236
    /**
1237
     * @param string $index
1238
     * @return object
1239
     * @throws \Exception
1240
     */
1241
    public static function getHeldObject($index)
1242
    {
1243
        if (isset(self::$objHolder[$index])) {
1244
            return self::$objHolder[$index];
1245
        }
1246
1247
        throw new \Exception("No object held for index '$index'");
1248
    }
1249
}
1250