Passed
Push — master ( b6cd05...9f5262 )
by Gaetano
05:39
created

Server::_xmlrpcs_multicall_do_call()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 11.7975

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 25
c 1
b 0
f 0
nc 11
nop 2
dl 0
loc 41
ccs 13
cts 16
cp 0.8125
crap 11.7975
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Exception\PhpXmlrpcException;
6
use PhpXmlRpc\Helper\Http;
7
use PhpXmlRpc\Helper\Logger;
8
use PhpXmlRpc\Helper\XMLParser;
9
use PhpXmlRpc\Traits\CharsetEncoderAware;
10
use PhpXmlRpc\Traits\LoggerAware;
11
use PhpXmlRpc\Traits\ParserAware;
12
13
/**
14
 * Allows effortless implementation of XML-RPC servers
15
 */
16
class Server
17
{
18
    use CharsetEncoderAware;
19
    use LoggerAware;
20
    use ParserAware;
21
22
    /**
23
     * @var string
24
     * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
25
     * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc).
26
     *
27
     * @todo create class constants for these
28
     */
29
    public $functions_parameters_type = 'xmlrpcvals';
30
31
    /**
32
     * @var array
33
     * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map
34
     * when the functions_parameters_type member is set to 'phpvals'.
35
     * @see Encoder::encode for a list of values
36
     */
37
    public $phpvals_encoding_options = array('auto_dates');
38
39
    /**
40
     * @var int
41
     * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
42
     * SECURITY SENSITIVE!
43
     * Valid values:
44
     * 0 =
45
     * 1 =
46
     * 2 =
47
     * 3 =
48
     */
49
    public $debug = 1;
50
51
    /**
52
     * @var int
53
     * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
54
     * 0 = catch it and return an 'internal error' xml-rpc response (default)
55
     * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error
56
     *     corresponding to the exception, both its code and message.
57
     * 2 = allow the exception to float to the upper layers
58
     */
59
    public $exception_handling = 0;
60
61
    /**
62
     * @var bool
63
     * When set to true, it will enable HTTP compression of the response, in case the client has declared its support
64
     * for compression in the request.
65
     * Automatically set at constructor time.
66
     */
67
    public $compress_response = false;
68
69
    /**
70
     * @var string[]
71
     * List of http compression methods accepted by the server for requests. Automatically set at constructor time.
72
     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
73
     */
74
    public $accepted_compression = array();
75
76
    /**
77
     * @var bool
78
     * Shall we serve calls to system.* methods?
79
     */
80
    public $allow_system_funcs = true;
81
82
    /**
83
     * List of charset encodings natively accepted for requests.
84
     * Set at constructor time.
85
     * UNUSED so far...
86
     */
87
    public $accepted_charset_encodings = array();
88
89
    /**
90
     * @var string
91
     * Charset encoding to be used for response.
92
     * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
93
     * Can be:
94
     * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled),
95
     * - null (leave unspecified in response, convert output stream to US_ASCII),
96
     * - 'auto' (use client-specified charset encoding or same as request if request headers do not specify it (unless request is US-ASCII: then use library default anyway).
97
     * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
98
     */
99
    public $response_charset_encoding = '';
100
101
    /**
102
     * @var mixed
103
     * Extra data passed at runtime to method handling functions. Used only by EPI layer
104
     * @internal
105
     */
106
    public $user_data = null;
107
108
    /**
109
     * Array defining php functions exposed as xml-rpc methods by this server.
110
     * @var array[] $dmap
111
     */
112
    protected $dmap = array();
113
114
    /**
115 562
     * Storage for internal debug info.
116
     */
117 562
    protected $debug_info = '';
118 562
119
    protected static $_xmlrpc_debuginfo = '';
120 562
    protected static $_xmlrpcs_occurred_errors = '';
121
    protected static $_xmlrpcs_prev_ehandler = '';
122
123
    /**
124
     * @param array[] $dispatchMap the dispatch map with definition of exposed services
125
     *                             Array keys are the names of the method names.
126
     *                             Each array value is an array with the following members:
127
     *                             - function (callable)
128 2
     *                             - docstring (optional)
129
     *                             - signature (array, optional)
130 2
     *                             - signature_docs (array, optional)
131 2
     *                             - parameters_type (string, optional)
132
     * @param boolean $serviceNow set to false to prevent the server from running upon construction
133 2
     */
134
    public function __construct($dispatchMap = null, $serviceNow = true)
135
    {
136
        // if ZLIB is enabled, let the server by default accept compressed requests,
137
        // and compress responses sent to clients that support them
138
        if (function_exists('gzinflate')) {
139
            $this->accepted_compression[] = 'gzip';
140
        }
141
        if (function_exists('gzuncompress')) {
142
            $this->accepted_compression[] = 'deflate';
143
        }
144
        if (function_exists('gzencode') || function_exists('gzcompress')) {
145
            $this->compress_response = true;
146
        }
147
148
        // by default the xml parser can support these 3 charset encodings
149
        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
150
151
        // dispMap is a dispatch array of methods mapped to function names and signatures.
152 562
        // If a method doesn't appear in the map then an unknown method error is generated.
153
        // milosch - changed to make passing dispMap optional. Instead, you can use the class add_to_map() function
154
        // to add functions manually (borrowed from SOAPX4)
155
        if ($dispatchMap) {
156 562
            $this->setDispatchMap($dispatchMap);
157 562
            if ($serviceNow) {
158 562
                $this->service();
159
            }
160
        }
161
    }
162 562
163
    /**
164
     * Set debug level of server.
165
     *
166
     * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments)
167
     *                    0 = no debug info,
168
     *                    1 = msgs set from user with debugmsg(),
169
     *                    2 = add complete xml-rpc request (headers and body),
170 562
     *                    3 = add also all processing warnings happened during method processing
171 561
     *                    (NB: this involves setting a custom error handler, and might interfere
172 561
     *                    with the standard processing of the php function exposed as method. In
173 2
     *                    particular, triggering a USER_ERROR level error will not halt script
174
     *                    execution anymore, but just end up logged in the xml-rpc response)
175
     *                    Note that info added at level 2 and 3 will be base64 encoded
176 562
     * @return $this
177
     */
178
    public function setDebug($level)
179
    {
180
        $this->debug = $level;
181
        return $this;
182
    }
183
184
    /**
185
     * Add a string to the debug info that can be later serialized by the server as part of the response message.
186
     * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
187
     * character set.
188
     *
189
     * @param string $msg
190
     * @return void
191
     */
192 559
    public static function xmlrpc_debugmsg($msg)
193
    {
194 559
        static::$_xmlrpc_debuginfo .= $msg . "\n";
195 559
    }
196
197
    /**
198
     * Add a string to the debug info that will be later serialized by the server as part of the response message
199
     * (base64 encoded, only when debug level >= 2)
200
     *
201
     * @param string $msg
202
     * @return void
203
     */
204 2
    public static function error_occurred($msg)
205
    {
206 2
        static::$_xmlrpcs_occurred_errors .= $msg . "\n";
207 2
    }
208
209
    /**
210
     * Return a string with the serialized representation of all debug info.
211
     *
212
     * @internal this function will become protected in the future
213
     *
214
     * @param string $charsetEncoding the target charset encoding for the serialization
215
     *
216 22
     * @return string an XML comment (or two)
217
     */
218 22
    public function serializeDebug($charsetEncoding = '')
219 22
    {
220
        // Tough encoding problem: which internal charset should we assume for debug info?
221
        // It might contain a copy of raw data received from client, ie with unknown encoding,
222
        // intermixed with php generated data and user generated data...
223
        // so we split it: system debug is base 64 encoded,
224
        // user debug info should be encoded by the end user using the INTERNAL_ENCODING
225
        $out = '';
226
        if ($this->debug_info != '') {
227
            $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
228 561
        }
229
        if (static::$_xmlrpc_debuginfo != '') {
230
            $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
231
            // NB: a better solution MIGHT be to use CDATA, but we need to insert it
232
            // into return payload AFTER the beginning tag
233
            //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
234
        }
235 561
236 561
        return $out;
237 559
    }
238
239 561
    /**
240 2
     * Execute the xml-rpc request, printing the response.
241
     *
242
     * @param string $data the request body. If null, the http POST request will be examined
243
     * @param bool $returnPayload When true, return the response but do not echo it or any http header
244
     *
245
     * @return Response|string the response object (usually not used by caller...) or its xml serialization
246 561
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
247
     */
248
    public function service($data = null, $returnPayload = false)
249
    {
250
        if ($data === null) {
251
            $data = file_get_contents('php://input');
252
        }
253
        $rawData = $data;
254
255
        // reset internal debug info
256
        $this->debug_info = '';
257
258
        // Save what we received, before parsing it
259 561
        if ($this->debug > 1) {
260
            $this->debugmsg("+++GOT+++\n" . $data . "\n+++END+++");
261 561
        }
262 561
263
        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
264 561
        if (!$resp) {
265
            // this actually executes the request
266
            $resp = $this->parseRequest($data, $reqCharset);
267 561
268
            // save full body of request into response, for debugging purposes.
269
            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
270 561
            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
271 559
            ///       better: just avoid setting this, and set debug info of the received http request in the request
272
            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
273
            ///       Last but not least: the raw data might be of use to handler functions - but in decompressed form...
274 561
            $resp->raw_data = $rawData;
275 561
        }
276
277 561
        if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
278
            $this->debugmsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
279
                static::$_xmlrpcs_occurred_errors . "+++END+++");
280
        }
281
282 561
        $payload = $this->xml_header($respCharset);
283
        if ($this->debug > 0) {
284
            $payload = $payload . $this->serializeDebug($respCharset);
285 561
        }
286 22
287 22
        // Do not create response serialization if it has already happened. Helps to build json magic
288
        /// @todo what if the payload was created targeting a different charset than $respCharset?
289
        if (empty($resp->payload)) {
290 561
            $resp->serialize($respCharset);
291 561
        }
292 561
        $payload = $payload . $resp->payload;
293
294
        if ($returnPayload) {
295
            return $payload;
296 561
        }
297 561
298
        // if we get a warning/error that has output some text before here, then we cannot
299 561
        // add a new header. We cannot say we are sending xml, either...
300
        if (!headers_sent()) {
301 561
            header('Content-Type: ' . $resp->content_type);
302
            // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
303
            header("Vary: Accept-Charset");
304
305
            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
306
            // and php ini settings do not force it already
307 561
            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
308 561
            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
309
                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
310
                    $payload = gzencode($payload);
311 561
                    header("Content-Encoding: gzip");
312
                    header("Vary: Accept-Encoding");
313
                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
314
                    $payload = gzcompress($payload);
315
                    header("Content-Encoding: deflate");
316
                    header("Vary: Accept-Encoding");
317 561
                }
318 561
            }
319 104
320
            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
321 104
            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
322 52
            // responses up to 8000 bytes
323 52
            if ($phpNoSelfCompress) {
324 52
                header('Content-Length: ' . (int)strlen($payload));
325 52
            }
326 52
        } else {
327 52
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
328 52
        }
329
330
        print $payload;
331
332
        // return response, in case subclasses want it
333
        return $resp;
334
    }
335
336 561
    /**
337 561
     * Add a method to the dispatch map.
338
     *
339
     * @param string $methodName the name with which the method will be made available
340
     * @param callable $function the php function that will get invoked
341
     * @param array[] $sig the array of valid method signatures.
342
     *                     Each element is one signature: an array of strings with at least one element
343 561
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
344
     * @param string $doc method documentation
345
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
346 561
     *                        descriptions instead of types (one string for return type, one per param)
347
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
348
     * @return void
349
     *
350
     * @todo raise a warning if the user tries to register a 'system.' method
351
     */
352
    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false)
353
    {
354
        $this->dmap[$methodName] = array(
355
            'function' => $function,
356
            'docstring' => $doc,
357
        );
358
        if ($sig) {
359
            $this->dmap[$methodName]['signature'] = $sig;
360
        }
361
        if ($sigDoc) {
362
            $this->dmap[$methodName]['signature_docs'] = $sigDoc;
363
        }
364
        if ($parametersType) {
365
            $this->dmap[$methodName]['parameters_type'] = $parametersType;
366
        }
367
    }
368
369
    /**
370
     * Verify type and number of parameters received against a list of known signatures.
371
     *
372
     * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
373
     * @param array $sigs array of known signatures to match against
374
     * @return array int, string
375
     */
376
    protected function verifySignature($in, $sigs)
377
    {
378
        // check each possible signature in turn
379
        if (is_object($in)) {
380
            $numParams = $in->getNumParams();
381
        } else {
382
            $numParams = count($in);
383
        }
384
        foreach ($sigs as $curSig) {
385
            if (count($curSig) == $numParams + 1) {
386 536
                $itsOK = 1;
387
                for ($n = 0; $n < $numParams; $n++) {
388
                    if (is_object($in)) {
389 536
                        $p = $in->getParam($n);
390 536
                        if ($p->kindOf() == 'scalar') {
391
                            $pt = $p->scalartyp();
392
                        } else {
393
                            $pt = $p->kindOf();
394 536
                        }
395 536
                    } else {
396 536
                        $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
397 536
                    }
398 515
399 515
                    // param index is $n+1, as first member of sig is return type
400 515
                    if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
401 452
                        $itsOK = 0;
402
                        $pno = $n + 1;
403 148
                        $wanted = $curSig[$n + 1];
404
                        $got = $pt;
405
                        break;
406
                    }
407
                }
408
                if ($itsOK) {
409
                    return array(1, '');
410 515
                }
411 22
            }
412 22
        }
413 22
        if (isset($wanted)) {
414 22
            return array(0, "Wanted {$wanted}, got {$got} at param {$pno}");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pno does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $got does not seem to be defined for all execution paths leading up to this point.
Loading history...
415 22
        } else {
416
            return array(0, "No method signature matches number of parameters");
417
        }
418 536
    }
419 536
420
    /**
421
     * Parse http headers received along with xml-rpc request. If needed, inflate request.
422
     *
423 22
     * @return Response|null null on success or an error Response
424
     */
425
    protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
426 22
    {
427
        // check if $_SERVER is populated: it might have been disabled via ini file
428
        // (this is true even when in CLI mode)
429
        if (count($_SERVER) == 0) {
430
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
431
        }
432
433
        if ($this->debug > 1) {
434
            if (function_exists('getallheaders')) {
435 561
                $this->debugmsg(''); // empty line
436
                foreach (getallheaders() as $name => $val) {
437
                    $this->debugmsg("HEADER: $name: $val");
438
                }
439 561
            }
440
        }
441
442
        if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
443 561
            $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
444 559
        } else {
445 559
            $contentEncoding = '';
446 559
        }
447 559
448
        $rawData = $data;
449
450
        // check if request body has been compressed and decompress it
451
        if ($contentEncoding != '' && strlen($data)) {
452 561
            if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
453 104
                // if decoding works, use it. else assume data wasn't gzencoded
454
                /// @todo test separately for gzinflate and gzuncompress
455 457
                if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
456
                    if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
457
                        $data = $degzdata;
458 561
                        if ($this->debug > 1) {
459
                            $this->debugmsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
460
                        }
461 561
                    } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
462 104
                        $data = $degzdata;
463
                        if ($this->debug > 1) {
464 104
                            $this->debugmsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
465 104
                        }
466 52
                    } else {
467 52
                        $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
468 52
                            PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
469
                        );
470 52
471 52
                        return $r;
472 52
                    }
473 52
                } else {
474
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
475
                        PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
476
                    );
477
478
                    return $r;
479
                }
480
            }
481
        }
482
483
        // check if client specified accepted charsets, and if we know how to fulfill the request
484
        if ($this->response_charset_encoding == 'auto') {
485
            $respEncoding = '';
486
            if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
487
                // here we check if we can match the client-requested encoding with the encodings we know we can generate.
488
                // we parse q=0.x preferences instead of preferring the first charset specified
489
                $http = new Http();
490
                $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
491
                $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
492
                foreach ($clientAcceptedCharsets as $accepted) {
493
                    foreach ($knownCharsets as $charset) {
494 561
                        if (strtoupper($accepted) == strtoupper($charset)) {
495
                            $respEncoding = $charset;
496
                            break 2;
497
                        }
498
                    }
499
                }
500
            }
501
        } else {
502
            $respEncoding = $this->response_charset_encoding;
503
        }
504
505
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
506
            $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
507
        } else {
508
            $respCompression = '';
509
        }
510
511
        // 'guestimate' request encoding
512
        /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
513
        $reqEncoding = XMLParser::guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
514
            $data);
515
516 561
        return null;
517
    }
518
519 561
    /**
520 104
     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
521
     * server.
522 457
     * @internal this function will become protected in the future
523
     *
524
     * @param string $data the xml request
525
     * @param string $reqEncoding (optional) the charset encoding of the xml request
526
     * @return Response
527 561
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
528
     *
529
     * @todo either rename this function or move the 'execute' part out of it...
530 561
     */
531
    public function parseRequest($data, $reqEncoding = '')
532
    {
533
        // decompose incoming XML into request structure
534
535
        /// @todo move this block of code into the XMLParser
536
        if ($reqEncoding != '') {
537
            // Since parsing will fail if
538
            // - charset is not specified in the xml declaration,
539
            // - the encoding is not UTF8 and
540
            // - there are non-ascii chars in the text,
541
            // we try to work round that...
542
            // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
543
            //if (!is_valid_charset($reqEncoding, array('UTF-8')))
544
            if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
545
                if (function_exists('mb_convert_encoding')) {
546
                    $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
547 562
                } else {
548
                    if ($reqEncoding == 'ISO-8859-1') {
549
                        $data = utf8_encode($data);
550
                    } else {
551 562
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
552
                    }
553
                }
554
            }
555
        }
556
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
557
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
558
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
559
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
560 561
        } else {
561 4
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
562 2
        }
563
        // register a callback with the xml parser for when it finds the method name
564 2
        $options['methodname_callback'] = array($this, 'methodNameCallback');
565 2
566
        $xmlRpcParser = $this->getParser();
567
        try {
568
            $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
569
        } catch (PhpXmlrpcException $e) {
570
            return new Response(0, $e->getCode(), $e->getMessage());
571
        }
572
573
        if ($xmlRpcParser->_xh['isf'] == 3) {
574
            // (BC) we return XML error as a faultCode
575
            preg_match('/^XML error ([0-9]+)/', $xmlRpcParser->_xh['isf_reason'], $matches);
576
            return new Response(
577 562
                0,
578
                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
579
                $xmlRpcParser->_xh['isf_reason']);
580
        } elseif ($xmlRpcParser->_xh['isf']) {
581 562
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc,
582
            ///       parsing error
583
            return new Response(
584 562
                0,
585 562
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
586 562
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
587
        } else {
588 2
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
589 2
            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
590 2
            // that would mean a useless encode+decode pass
591 2
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
592 560
                (isset($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type']) &&
593 1
                    ($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type'] != 'xmlrpcvals')
594 1
                )
595 1
            ) {
596
                if ($this->debug > 1) {
597
                    $this->debugmsg("\n+++PARSED+++\n" . var_export($xmlRpcParser->_xh['params'], true) . "\n+++END+++");
598
                }
599
600
                return $this->execute($xmlRpcParser->_xh['method'], $xmlRpcParser->_xh['params'], $xmlRpcParser->_xh['pt']);
601 559
            } else {
602 559
                // build a Request object with data parsed from xml and add parameters in
603
                $req = new Request($xmlRpcParser->_xh['method']);
604
                for ($i = 0; $i < count($xmlRpcParser->_xh['params']); $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...
605
                    $req->addParam($xmlRpcParser->_xh['params'][$i]);
606
                }
607
608
                if ($this->debug > 1) {
609
                    $this->debugmsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
610
                }
611
612 559
                return $this->execute($req);
613
            }
614 559
        }
615 538
    }
616
617
    /**
618 559
     * Execute a method invoked by the client, checking parameters used.
619 559
     *
620
     * @param Request|string $req either a Request obj or a method name
621 559
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
622
     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
623
     * @return Response
624
     *
625 562
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
626
     */
627
    protected function execute($req, $params = null, $paramTypes = null)
628
    {
629
        static::$_xmlrpcs_occurred_errors = '';
630
        static::$_xmlrpc_debuginfo = '';
631
632
        if (is_object($req)) {
633
            $methodName = $req->method();
634
        } else {
635
            $methodName = $req;
636
        }
637
638
        $sysCall = $this->isSyscall($methodName);
639 559
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
640
641 559
        if (!isset($dmap[$methodName]['function'])) {
642 559
            // No such method
643
            return new Response(0,
644 559
                PhpXmlRpc::$xmlrpcerr['unknown_method'],
645 559
                PhpXmlRpc::$xmlrpcstr['unknown_method']);
646
        }
647
648
        // Check signature
649 559
        if (isset($dmap[$methodName]['signature'])) {
650 559
            $sig = $dmap[$methodName]['signature'];
651
            if (is_object($req)) {
652 559
                list($ok, $errStr) = $this->verifySignature($req, $sig);
653
            } else {
654 85
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
655 85
            }
656 85
            if (!$ok) {
657
                // Didn't match.
658
                return new Response(
659
                    0,
660 559
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
661 536
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
662 536
                );
663 536
            }
664
        }
665
666
        $func = $dmap[$methodName]['function'];
667 536
668
        // let the 'class::function' syntax be accepted in dispatch maps
669 22
        if (is_string($func) && strpos($func, '::')) {
670 22
            $func = explode('::', $func);
671 22
        }
672 22
673
        // build string representation of function 'name'
674
        if (is_array($func)) {
675
            if (is_object($func[0])) {
676
                $funcName = get_class($func[0]) . '->' . $func[1];
677 559
            } else {
678
                $funcName = implode('::', $func);
679 559
            }
680 127
        } else if ($func instanceof \Closure) {
681
            $funcName = 'Closure';
682
        } else {
683 559
            $funcName = $func;
684 150
        }
685 24
686
        // verify that function to be invoked is in fact callable
687 127
        if (!is_callable($func)) {
688
            $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
689 431
            return new Response(
690 129
                0,
691
                PhpXmlRpc::$xmlrpcerr['server_error'],
692 324
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
693
            );
694
        }
695
696 559
        // If debug level is 3, we should catch all errors generated during processing of user function, and log them
697
        // as part of response
698
        if ($this->debug > 2) {
699
            self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
700
        }
701
702
        try {
703
            // Allow mixed-convention servers
704
            if (is_object($req)) {
705
                // call an 'xml-rpc aware' function
706
                if ($sysCall) {
707 559
                    $r = call_user_func($func, $this, $req);
708 559
                } else {
709
                    $r = call_user_func($func, $req);
710
                }
711
                if (!is_a($r, 'PhpXmlRpc\Response')) {
712
                    $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
713 559
                    if (is_a($r, 'PhpXmlRpc\Value')) {
714 559
                        $r = new Response($r);
715 127
                    } else {
716
                        $r = new Response(
717 454
                            0,
718
                            PhpXmlRpc::$xmlrpcerr['server_error'],
719 557
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
720
                        );
721
                    }
722
                }
723
            } else {
724
                // call a 'plain php' function
725
                if ($sysCall) {
726
                    array_unshift($params, $this);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $array of array_unshift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

726
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
727
                    $r = call_user_func_array($func, $params);
728
                } else {
729
                    // 3rd API convention for method-handling functions: EPI-style
730
                    if ($this->functions_parameters_type == 'epivals') {
731
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
732
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
733
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
734
                            $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
735
                        } else {
736
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
737
                            // return type correctly
738
                            $encoder = new Encoder();
739
                            $r = new Response($encoder->encode($r, array('extension_api')));
740
                        }
741
                    } else {
742
                        $r = call_user_func_array($func, $params);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $args of call_user_func_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

742
                        $r = call_user_func_array($func, /** @scrutinizer ignore-type */ $params);
Loading history...
743
                    }
744
                }
745
                // the return type can be either a Response object or a plain php value...
746
                if (!is_a($r, '\PhpXmlRpc\Response')) {
747
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
748
                    // a: let the user decide
749
                    $encoder = new Encoder();
750
                    $r = new Response($encoder->encode($r, $this->phpvals_encoding_options));
751
                }
752
            }
753
        } catch (\Exception $e) {
754
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
755
            // proper error-response
756
            switch ($this->exception_handling) {
757
                case 2:
758
                    if ($this->debug > 2) {
759
                        if (self::$_xmlrpcs_prev_ehandler) {
760
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
761
                        } else {
762 45
                            restore_error_handler();
763
                        }
764
                    }
765 45
                    throw $e;
766 45
                case 1:
767
                    $errCode = $e->getCode();
768
                    if ($errCode == 0) {
769
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
770
                    }
771
                    $r = new Response(0, $errCode, $e->getMessage());
772
                    break;
773
                default:
774
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
775 45
            }
776 2
        } catch (\Error $e) {
777 2
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
778
            // proper error-response
779 45
            switch ($this->exception_handling) {
780
                case 2:
781
                    if ($this->debug > 2) {
782 559
                        if (self::$_xmlrpcs_prev_ehandler) {
783
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
784
                        } else {
785 559
                            restore_error_handler();
786 64
                        }
787
                    }
788 496
                    throw $e;
789
                case 1:
790
                    $errCode = $e->getCode();
791
                    if ($errCode == 0) {
792 559
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
793
                    }
794
                    $r = new Response(0, $errCode, $e->getMessage());
795
                    break;
796
                default:
797
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
798
            }
799
        }
800 559
801
        if ($this->debug > 2) {
802 559
            // note: restore the error handler we found before calling the user func, even if it has been changed
803 559
            // inside the func itself
804
            if (self::$_xmlrpcs_prev_ehandler) {
805
                set_error_handler(self::$_xmlrpcs_prev_ehandler);
806
            } else {
807
                restore_error_handler();
808
            }
809 561
        }
810
811 561
        return $r;
812 52
    }
813
814 509
    /**
815
     * Registered as callback for when the XMLParser has found the name of the method to execute.
816
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
817
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
818
     *
819
     * @internal
820
     * @param $methodName
821
     * @param XMLParser $xmlParser
822 559
     * @param resource $parser
823
     * @return void
824 559
     * @throws PhpXmlrpcException
825
     *
826
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
827
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
828
     *       about the matched method handler, in order to avoid doing the work twice...
829
     */
830
    public function methodNameCallback($methodName, $xmlParser, $parser)
831
    {
832
        $sysCall = $this->isSyscall($methodName);
833
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
834
835
        if (!isset($dmap[$methodName]['function'])) {
836
            // No such method
837
            throw new PhpXmlrpcException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
838 127
        }
839
840 127
        // alter on-the-fly the config of the xml parser if needed
841
        if (isset($dmap[$methodName]['parameters_type']) &&
842
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
843
            /// @todo this should be done by a method of the XMLParser
844
            switch ($dmap[$methodName]['parameters_type']) {
845
                case XMLParser::RETURN_PHP:
846 127
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
847
                    break;
848
                case XMLParser::RETURN_EPIVALS:
849 127
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
850 127
                    break;
851
                /// @todo log a warning on unsupported return type
852
                case XMLParser::RETURN_XMLRPCVALS:
853
                default:
854 127
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
855 127
            }
856 127
        }
857
    }
858
859
    /**
860 127
     * Add a string to the 'internal debug message' (separate from 'user debug message').
861 127
     *
862 127
     * @param string $string
863
     * @return void
864
     */
865
    protected function debugmsg($string)
866 127
    {
867 127
        $this->debug_info .= $string . "\n";
868 127
    }
869
870
    /**
871
     * @param string $charsetEncoding
872 127
     * @return string
873 127
     */
874 127
    protected function xml_header($charsetEncoding = '')
875
    {
876
        if ($charsetEncoding != '') {
877
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
878
        } else {
879
            return "<?xml version=\"1.0\"?" . ">\n";
880
        }
881
    }
882
883
    /**
884
     * @param string $methName
885
     * @return bool
886
     */
887
    protected function isSyscall($methName)
888
    {
889
        return (strpos($methName, "system.") === 0);
890
    }
891
892
893
    /**
894
     * @param array $dmap
895
     * @return $this
896
     */
897
    public function setDispatchMap($dmap)
898
    {
899
        $this->dmap = $dmap;
900
        return $this;
901
    }
902
903
    /**
904
     * @return array[]
905
     */
906
    public function getDispatchMap()
907
    {
908
        return $this->dmap;
909
    }
910
911
    /**
912
     * @return array[]
913
     */
914
    public function getSystemDispatchMap()
915
    {
916
        if (!$this->allow_system_funcs) {
917
            return array();
918
        }
919
920
        return array(
921
            'system.listMethods' => array(
922
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
923
                // listMethods: signature was either a string, or nothing.
924
                // The useless string variant has been removed
925
                'signature' => array(array(Value::$xmlrpcArray)),
926
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
927
                'signature_docs' => array(array('list of method names')),
928
            ),
929
            'system.methodHelp' => array(
930
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
931
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
932
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
933 22
                'signature_docs' => array(array('method description', 'name of the method to be described')),
934
            ),
935 22
            'system.methodSignature' => array(
936 22
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
937 22
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
938
                'docstring' => 'Returns an array of known signatures (an array of arrays) for the method name passed. If no signatures are known, returns a none-array (test for type != array to detect missing signature)',
939 22
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
940 22
            ),
941
            'system.multicall' => array(
942
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
943 22
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
944
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
945
                'signature_docs' => array(array('list of response structs, where each struct has the usual members', 'list of calls, with each call being represented as a struct, with members "methodname" and "params"')),
946
            ),
947
            'system.getCapabilities' => array(
948
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
949
                'signature' => array(array(Value::$xmlrpcStruct)),
950
                'docstring' => 'This method lists all the capabilities that the XML-RPC server has: the (more or less standard) extensions to the xmlrpc spec that it adheres to',
951 106
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
952
            ),
953
        );
954 106
    }
955 106
956 106
    /**
957
     * @return array[]
958
     */
959
    public function getCapabilities()
960 106
    {
961 85
        $outAr = array(
962
            // xml-rpc spec: always supported
963 22
            'xmlrpc' => array(
964
                'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md
965 106
                'specVersion' => 1
966 106
            ),
967 106
            // if we support system.xxx functions, we always support multicall, too...
968 106
            'system.multicall' => array(
969 106
                // Note that, as of 2006/09/17, the following URL does not respond anymore
970 106
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
971 106
                'specVersion' => 1
972
            ),
973 106
            // introspection: version 2! we support 'mixed', too.
974
            // note: the php xml-rpc extension says this instead:
975 106
            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
976
            'introspection' => array(
977
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
978
                'specVersion' => 2,
979
            ),
980
        );
981
982 1
        // NIL extension
983
        if (PhpXmlRpc::$xmlrpc_null_extension) {
984
            $outAr['nil'] = array(
985 106
                // Note that, as of 2023/01, the following URL does not respond anymore
986
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
987
                'specVersion' => 1
988
            );
989
        }
990
991
        /// @todo add support for "standard" error codes
992
        //if (...) {
993 85
        //    $outAr['faults_interop'] = array(
994
        //        'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
995
        //        'specVersion' => 20010516
996 85
        //    );
997 85
        //}
998 85
999
        return $outAr;
1000
    }
1001
1002 85
    /**
1003 85
     * @internal handler of a system. method
1004
     *
1005 1
     * @param Server $server
1006
     * @param Request $req
1007 85
     * @return Response
1008 85
     */
1009 85
    public static function _xmlrpcs_getCapabilities($server, $req = null)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed. ( Ignorable by Annotation )

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

1009
    public static function _xmlrpcs_getCapabilities($server, /** @scrutinizer ignore-unused */ $req = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1010
    {
1011
        $encoder = new Encoder();
1012
        return new Response($encoder->encode($server->getCapabilities()));
1013
    }
1014
1015
    /**
1016
     * @internal handler of a system. method
1017 85
     *
1018
     * @param Server $server
1019
     * @param Request $req if called in plain php values mode, second param is missing
1020 64
     * @return Response
1021
     */
1022 64
    public static function _xmlrpcs_listMethods($server, $req = null)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed. ( Ignorable by Annotation )

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

1022
    public static function _xmlrpcs_listMethods($server, /** @scrutinizer ignore-unused */ $req = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1023 64
    {
1024 64
        $outAr = array();
1025
        foreach ($server->dmap as $key => $val) {
1026 64
            $outAr[] = new Value($key, 'string');
1027 64
        }
1028
        foreach ($server->getSystemDispatchMap() as $key => $val) {
1029 64
            $outAr[] = new Value($key, 'string');
1030 64
        }
1031 64
1032
        return new Response(new Value($outAr, 'array'));
1033 64
    }
1034
1035
    /**
1036
     * @internal handler of a system. method
1037
     *
1038
     * @param Server $server
1039
     * @param Request $req
1040
     * @return Response
1041 64
     */
1042
    public static function _xmlrpcs_methodSignature($server, $req)
1043 64
    {
1044
        // let's accept as parameter either an xml-rpc value or string
1045
        if (is_object($req)) {
1046 64
            $methName = $req->getParam(0);
1047 64
            $methName = $methName->scalarval();
1048
        } else {
1049
            $methName = $req;
1050 64
        }
1051
        if ($server->isSyscall($methName)) {
1052
            $dmap = $server->getSystemDispatchMap();
1053 64
        } else {
1054 64
            $dmap = $server->dmap;
1055
        }
1056
        if (isset($dmap[$methName])) {
1057 64
            if (isset($dmap[$methName]['signature'])) {
1058 64
                $sigs = array();
1059
                foreach ($dmap[$methName]['signature'] as $inSig) {
1060
                    $curSig = array();
1061 64
                    foreach ($inSig as $sig) {
1062
                        $curSig[] = new Value($sig, 'string');
1063
                    }
1064
                    $sigs[] = new Value($curSig, 'array');
1065 64
                }
1066 64
                $r = new Response(new Value($sigs, 'array'));
1067 64
            } else {
1068
                // NB: according to the official docs, we should be returning a
1069
                // "none-array" here, which means not-an-array
1070
                $r = new Response(new Value('undef', 'string'));
1071
            }
1072
        } else {
1073
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1074
        }
1075 64
1076
        return $r;
1077 64
    }
1078 64
1079
    /**
1080
     * @internal handler of a system. method
1081 64
     *
1082
     * @param Server $server
1083
     * @param Request $req
1084
     * @return Response
1085
     */
1086
    public static function _xmlrpcs_methodHelp($server, $req)
1087
    {
1088
        // let's accept as parameter either an xml-rpc value or string
1089
        if (is_object($req)) {
1090
            $methName = $req->getParam(0);
1091
            $methName = $methName->scalarval();
1092
        } else {
1093
            $methName = $req;
1094
        }
1095
        if ($server->isSyscall($methName)) {
1096
            $dmap = $server->getSystemDispatchMap();
1097
        } else {
1098
            $dmap = $server->dmap;
1099
        }
1100
        if (isset($dmap[$methName])) {
1101
            if (isset($dmap[$methName]['docstring'])) {
1102
                $r = new Response(new Value($dmap[$methName]['docstring'], 'string'));
1103
            } else {
1104
                $r = new Response(new Value('', 'string'));
1105
            }
1106
        } else {
1107
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1108
        }
1109
1110
        return $r;
1111
    }
1112
1113
    /**
1114
     * @internal this function will become protected in the future
1115
     *
1116
     * @param $err
1117
     * @return Value
1118
     */
1119
    public static function _xmlrpcs_multicall_error($err)
1120
    {
1121
        if (is_string($err)) {
1122
            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
1123
            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
1124
        } else {
1125
            $code = $err->faultCode();
1126
            $str = $err->faultString();
1127
        }
1128
        $struct = array();
1129
        $struct['faultCode'] = new Value($code, 'int');
1130
        $struct['faultString'] = new Value($str, 'string');
1131
1132
        return new Value($struct, 'struct');
1133
    }
1134
1135
    /**
1136
     * @internal this function will become protected in the future
1137 85
     *
1138
     * @param Server $server
1139 85
     * @param Value $call
1140
     * @return Value
1141 85
     */
1142 85
    public static function _xmlrpcs_multicall_do_call($server, $call)
1143 85
    {
1144 64
        if ($call->kindOf() != 'struct') {
1145
            return static::_xmlrpcs_multicall_error('notstruct');
1146
        }
1147
        $methName = @$call['methodName'];
1148
        if (!$methName) {
1149
            return static::_xmlrpcs_multicall_error('nomethod');
1150
        }
1151
        if ($methName->kindOf() != 'scalar' || $methName->scalartyp() != 'string') {
1152
            return static::_xmlrpcs_multicall_error('notstring');
1153 85
        }
1154
        if ($methName->scalarval() == 'system.multicall') {
1155
            return static::_xmlrpcs_multicall_error('recursion');
1156
        }
1157
1158
        $params = @$call['params'];
1159
        if (!$params) {
1160
            return static::_xmlrpcs_multicall_error('noparams');
1161
        }
1162
        if ($params->kindOf() != 'array') {
1163
            return static::_xmlrpcs_multicall_error('notarray');
1164 43
        }
1165
1166
        $req = new Request($methName->scalarval());
1167 43
        foreach ($params as $i => $param) {
1168 22
            if (!$req->addParam($param)) {
1169
                $i++; // for error message, we count params from 1
1170
                return static::_xmlrpcs_multicall_error(new Response(0,
1171
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1172 22
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1173 22
            }
1174
        }
1175
1176
        $result = $server->execute($req);
1177 22
1178
        if ($result->faultCode() != 0) {
1179
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1180 22
        }
1181 22
1182 22
        return new Value(array($result->value()), 'array');
1183
    }
1184 22
1185
    /**
1186
     * @internal this function will become protected in the future
1187
     *
1188
     * @param Server $server
1189
     * @param Value $call
1190
     * @return Value
1191
     */
1192
    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1193
    {
1194
        if (!is_array($call)) {
0 ignored issues
show
introduced by
The condition is_array($call) is always false.
Loading history...
1195
            return static::_xmlrpcs_multicall_error('notstruct');
1196
        }
1197
        if (!array_key_exists('methodName', $call)) {
1198 22
            return static::_xmlrpcs_multicall_error('nomethod');
1199
        }
1200
        if (!is_string($call['methodName'])) {
1201
            return static::_xmlrpcs_multicall_error('notstring');
1202
        }
1203
        if ($call['methodName'] == 'system.multicall') {
1204
            return static::_xmlrpcs_multicall_error('recursion');
1205
        }
1206
        if (!array_key_exists('params', $call)) {
1207
            return static::_xmlrpcs_multicall_error('noparams');
1208
        }
1209
        if (!is_array($call['params'])) {
1210
            return static::_xmlrpcs_multicall_error('notarray');
1211
        }
1212
1213
        // this is a simplistic hack, since we might have received
1214
        // base64 or datetime values, but they will be listed as strings here...
1215
        $pt = array();
1216
        $wrapper = new Wrapper();
1217
        foreach ($call['params'] as $val) {
1218
            // support EPI-encoded base64 and datetime values
1219
            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
1220
                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
1221
            } else {
1222
                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
1223
            }
1224
        }
1225
1226
        $result = $server->execute($call['methodName'], $call['params'], $pt);
1227
1228
        if ($result->faultCode() != 0) {
1229
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1230
        }
1231
1232
        return new Value(array($result->value()), 'array');
1233
    }
1234
1235
    /**
1236
     * @internal handler of a system. method
1237
     *
1238
     * @param Server $server
1239
     * @param Request|array $req
1240
     * @return Response
1241
     */
1242
    public static function _xmlrpcs_multicall($server, $req)
1243
    {
1244
        $result = array();
1245
        // let accept a plain list of php parameters, beside a single xml-rpc msg object
1246
        if (is_object($req)) {
1247
            $calls = $req->getParam(0);
1248
            foreach ($calls as $call) {
1249
                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
1250
            }
1251
        } else {
1252
            $numCalls = count($req);
1253
            for ($i = 0; $i < $numCalls; $i++) {
1254
                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
1255
            }
1256
        }
1257
1258
        return new Response(new Value($result, 'array'));
1259
    }
1260
1261
    /**
1262
     * Error handler used to track errors that occur during server-side execution of PHP code.
1263
     * This allows to report back to the client whether an internal error has occurred or not
1264
     * using an xml-rpc response object, instead of letting the client deal with the html junk
1265
     * that a PHP execution error on the server generally entails.
1266
     *
1267
     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
1268
     *
1269
     * @internal
1270
     */
1271
    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
1272
    {
1273
        // obey the @ protocol
1274
        if (error_reporting() == 0) {
1275
            return;
1276
        }
1277
1278
        //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
1279
        if ($errCode != E_STRICT) {
1280
            static::error_occurred($errString);
1281
        }
1282
        // Try to avoid as much as possible disruption to the previous error handling
1283
        // mechanism in place
1284
        if (self::$_xmlrpcs_prev_ehandler == '') {
1285
            // The previous error handler was the default: all we should do is log error
1286
            // to the default error log (if level high enough)
1287
            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
1288
                if (self::$logger === null) {
1289
                    self::$logger = Logger::instance();
1290
                }
1291
                self::$logger->errorLog($errString);
1292
            }
1293
        } else {
1294
            // Pass control on to previous error handler, trying to avoid loops...
1295
            if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) {
0 ignored issues
show
introduced by
The condition self::_xmlrpcs_prev_ehan..._xmlrpcs_errorHandler') is always true.
Loading history...
1296
                if (is_array(self::$_xmlrpcs_prev_ehandler)) {
0 ignored issues
show
introduced by
The condition is_array(self::_xmlrpcs_prev_ehandler) is always false.
Loading history...
1297
                    // the following works both with static class methods and plain object methods as error handler
1298
                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
1299
                } else {
1300
                    $method = self::$_xmlrpcs_prev_ehandler;
1301
                    $method($errCode, $errString, $filename, $lineNo, $context);
1302
                }
1303
            }
1304
        }
1305
    }
1306
}
1307