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

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

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