Wrapper   F
last analyzed

Complexity

Total Complexity 270

Size/Duplication

Total Lines 1235
Duplicated Lines 0 %

Test Coverage

Coverage 83.96%

Importance

Changes 16
Bugs 2 Features 1
Metric Value
eloc 601
c 16
b 2
f 1
dl 0
loc 1235
ccs 445
cts 530
cp 0.8396
rs 1.999
wmc 270

19 Methods

Rating   Name   Duplication   Size   Complexity  
B newFunctionName() 0 31 7
F buildWrapFunctionSource() 0 95 21
A retrieveMethodHelp() 0 26 6
A buildClientWrapperCode() 0 18 5
D buildWrapMethodClosure() 0 92 30
A getHeldObject() 0 7 2
B buildMethodSignatures() 0 56 9
D wrapPhpClass() 0 25 22
A wrapXmlrpcMethod() 0 25 5
A generateMethodNameForClassMethod() 0 12 6
F buildWrapMethodSource() 0 111 37
C buildWrapFunctionClosure() 0 60 14
A holdObject() 0 3 1
D php2XmlrpcType() 0 37 20
C xmlrpc2PhpType() 0 23 13
D introspectFunction() 0 96 21
F wrapXmlrpcServer() 0 101 26
B retrieveMethodSignature() 0 36 9
C wrapPhpFunction() 0 62 16

How to fix   Complexity   

Complex Class

Complex classes like Wrapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Wrapper, and based on these observations, apply Extract Interface, too.

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\Exception\ValueErrorException;
11
use PhpXmlRpc\Traits\LoggerAware;
12
13
/**
14
 * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa.
15
 * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies.
16
 *
17
 * @todo use some better templating system for code generation?
18
 * @todo implement method wrapping with preservation of php objs in calls
19
 * @todo add support for 'epivals' mode
20
 * @todo allow setting custom namespace for generated wrapping code
21
 */
22
class Wrapper
23
{
24
    use LoggerAware;
25
26
    /**
27
     * @var object[]
28
     * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode
29 24
     * @internal this property will become protected in the future
30
     */
31 24
    public static $objHolder = array();
32 1
33
    /** @var string */
34 24
    protected static $namespace = '\\PhpXmlRpc\\';
35
36
    /**
37
     * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
38
     * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
39
     * Notes:
40
     * - for php 'resource' types returns empty string, since resources cannot be serialized;
41
     * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs
42
     * - for php arrays always return array, even though arrays sometimes serialize as structs...
43
     * - for 'void' and 'null' returns 'undefined'
44
     *
45
     * @param string $phpType
46
     * @return string
47
     *
48
     * @todo support notation `something[]` as 'array'
49
     * @todo check if nil support is enabled when finding null
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 error logging when returning false?
163
     * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
164
     * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
165
     * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
166
     * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway
167
     */
168
    public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array())
169
    {
170
        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
171
172
        if (is_string($callable) && strpos($callable, '::') !== false) {
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()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong');
178
                return false;
179 559
            }
180
            if (is_string($callable[0])) {
181 559
                $plainFuncName = implode('::', $callable);
182 559
            } elseif (is_object($callable[0])) {
183
                $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
184 559
            }
185 559
            $exists = method_exists($callable[0], $callable[1]);
186
        } else if ($callable instanceof \Closure) {
187
            // we do not support creating code which wraps closures, as php does not allow to serialize them
188
            if (!$buildIt) {
189 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code');
190 559
                return false;
191 559
            }
192 559
193
            $plainFuncName = 'Closure';
194 559
            $exists = true;
195 559
        } else {
196
            $plainFuncName = $callable;
197 559
            $exists = function_exists($callable);
198
        }
199
200
        if (!$exists) {
201
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName);
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()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
246
                return false;
247
            }
248 559
            if ($func->isProtected()) {
249
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
250
                return false;
251 559
            }
252 559
            if ($func->isConstructor()) {
253 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
254
                return false;
255
            }
256
            if ($func->isDestructor()) {
257 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
258
                return false;
259
            }
260
            if ($func->isAbstract()) {
261 559
                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName);
262
                return false;
263
            }
264
            /// @todo add more checks for static vs. nonstatic?
265 559
        } else {
266
            $func = new \ReflectionFunction($callable);
267
        }
268
        if ($func->isInternal()) {
269 559
            /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully
270
            ///       reflect internal php functions
271
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName);
272
            return false;
273
        }
274
275 559
        // retrieve parameter names, types and description from javadoc comments
276
277 559
        // function description
278
        $desc = '';
279
        // type of return val: by default 'any'
280
        $returns = Value::$xmlrpcValue;
281
        // desc of return val
282
        $returnsDocs = '';
283
        // type + name of function parameters
284
        $paramDocs = array();
285
286
        $docs = $func->getDocComment();
287 559
        if ($docs != '') {
288
            $docs = explode("\n", $docs);
289 559
            $i = 0;
290
            foreach ($docs as $doc) {
291 559
                $doc = trim($doc, " \r\t/*");
292
                if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
293 559
                    if ($desc) {
294
                        $desc .= "\n";
295 559
                    }
296 559
                    $desc .= $doc;
297 559
                } elseif (strpos($doc, '@param') === 0) {
298 559
                    // syntax: @param type $name [desc]
299 559
                    if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) {
300 559
                        $name = strtolower(trim($matches[2]));
301 559
                        //$paramDocs[$name]['name'] = trim($matches[2]);
302 559
                        $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : '';
303 559
                        $paramDocs[$name]['type'] = $matches[1];
304
                    }
305 559
                    $i++;
306 559
                } elseif (strpos($doc, '@return') === 0) {
307
                    // syntax: @return type [desc]
308 559
                    if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
309 559
                        $returns = $matches[1];
310
                        if (isset($matches[2])) {
311 559
                            $returnsDocs = trim($matches[2]);
312 559
                        }
313
                    }
314 559
                }
315 559
            }
316
        }
317 559
318 559
        // 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
     * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on
638 559
     *       each invocation, for the case where all the generated functions will be saved as methods of a class
639 559
     */
640 559
    public function wrapPhpClass($className, $extraOptions = array())
641 559
    {
642
        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
643 559
        $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
644
645 559
        $results = array();
646 559
        $mList = get_class_methods($className);
647
        foreach ($mList as $mName) {
648
            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
649
                $func = new \ReflectionMethod($className, $mName);
650
                if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
651
                    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...
652
                        (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
653 559
                    ) {
654
                        $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

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