Passed
Push — master ( 327b5e...7cdbcc )
by Gaetano
05:53
created

Server::getSystemDispatchMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 38
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2.0008

Importance

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

781
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
782 559
                    $r = call_user_func_array($func, $params);
783
                } else {
784
                    // 3rd API convention for method-handling functions: EPI-style
785 559
                    if ($this->functions_parameters_type == 'epivals') {
786 64
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
787
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
788 496
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
789
                            $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
790
                        } else {
791
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
792 559
                            // return type correctly
793
                            $encoder = new Encoder();
794
                            $r = new Response($encoder->encode($r, array('extension_api')));
795
                        }
796
                    } else {
797
                        $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

797
                        $r = call_user_func_array($func, /** @scrutinizer ignore-type */ $params);
Loading history...
798
                    }
799
                }
800 559
                // the return type can be either a Response object or a plain php value...
801
                if (!is_a($r, '\PhpXmlRpc\Response')) {
802 559
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
803 559
                    // a: let the user decide
804
                    $encoder = new Encoder();
805
                    $r = new Response($encoder->encode($r, $this->phpvals_encoding_options));
806
                }
807
            }
808
        } catch (\Exception $e) {
809 561
            // (barring errors in the lib) an uncatched exception happened in the called function, we wrap it in a
810
            // proper error-response
811 561
            switch ($this->exception_handling) {
812 52
                case 2:
813
                    if ($this->debug > 2) {
814 509
                        if (self::$_xmlrpcs_prev_ehandler) {
815
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
816
                        } else {
817
                            restore_error_handler();
818
                        }
819
                    }
820
                    throw $e;
821
                case 1:
822 559
                    $errCode = $e->getCode();
823
                    if ($errCode == 0) {
824 559
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
825
                    }
826
                    $r = new Response(0, $errCode, $e->getMessage());
827
                    break;
828
                default:
829
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
830
            }
831
        } catch (\Error $e) {
832
            // (barring errors in the lib) an uncatched exception happened in the called function, we wrap it in a
833
            // proper error-response
834
            switch ($this->exception_handling) {
835
                case 2:
836
                    if ($this->debug > 2) {
837
                        if (self::$_xmlrpcs_prev_ehandler) {
838 127
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
839
                        } else {
840 127
                            restore_error_handler();
841
                        }
842
                    }
843
                    throw $e;
844
                case 1:
845
                    $errCode = $e->getCode();
846 127
                    if ($errCode == 0) {
847
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
848
                    }
849 127
                    $r = new Response(0, $errCode, $e->getMessage());
850 127
                    break;
851
                default:
852
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
853
            }
854 127
        }
855 127
856 127
        if ($this->debug > 2) {
857
            // note: restore the error handler we found before calling the user func, even if it has been changed
858
            // inside the func itself
859
            if (self::$_xmlrpcs_prev_ehandler) {
860 127
                set_error_handler(self::$_xmlrpcs_prev_ehandler);
861 127
            } else {
862 127
                restore_error_handler();
863
            }
864
        }
865
866 127
        return $r;
867 127
    }
868 127
869
    /**
870
     * Registered as callback for when the XMLParser has found the name of the method to execute.
871
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
872 127
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
873 127
     *
874 127
     * @internal
875
     * @param $methodName
876
     * @param XMLParser $xmlParser
877
     * @param resource $parser
878
     * @return void
879
     * @throws PhpXmlrpcException
880
     *
881
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
882
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
883
     *       about the matched method handler, in order to avoid doing the work twice...
884
     */
885
    public function methodNameCallback($methodName, $xmlParser, $parser)
886
    {
887
        $sysCall = $this->isSyscall($methodName);
888
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
889
890
        if (!isset($dmap[$methodName]['function'])) {
891
            // No such method
892
            throw new PhpXmlrpcException(0, PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
0 ignored issues
show
Bug introduced by
PhpXmlRpc\PhpXmlRpc::xmlrpcstr['unknown_method'] of type string is incompatible with the type integer expected by parameter $code of PhpXmlRpc\Exception\PhpX...xception::__construct(). ( Ignorable by Annotation )

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

892
            throw new PhpXmlrpcException(0, /** @scrutinizer ignore-type */ PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
Loading history...
Bug introduced by
PhpXmlRpc\PhpXmlRpc::xmlrpcerr['unknown_method'] of type integer is incompatible with the type Throwable|null expected by parameter $previous of PhpXmlRpc\Exception\PhpX...xception::__construct(). ( Ignorable by Annotation )

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

892
            throw new PhpXmlrpcException(0, PhpXmlRpc::$xmlrpcstr['unknown_method'], /** @scrutinizer ignore-type */ PhpXmlRpc::$xmlrpcerr['unknown_method']);
Loading history...
893
        }
894
895
        // alter on-the-fly the config of the xml parser if needed
896
        if (isset($dmap[$methodName]['parameters_type']) &&
897
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
898
            /// @todo this should be done by a method of the XMLParser
899
            switch ($dmap[$methodName]['parameters_type']) {
900
                case XMLParser::RETURN_PHP:
901
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
902
                    break;
903
                case XMLParser::RETURN_EPIVALS:
904
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
905
                    break;
906
                /// @todo log a warning on unsupported return type
907
                case XMLParser::RETURN_XMLRPCVALS:
908
                default:
909
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
910
            }
911
        }
912
    }
913
914
    /**
915
     * Add a string to the 'internal debug message' (separate from 'user debug message').
916
     *
917
     * @param string $string
918
     * @return void
919
     */
920
    protected function debugmsg($string)
921
    {
922
        $this->debug_info .= $string . "\n";
923
    }
924
925
    /**
926
     * @param string $charsetEncoding
927
     * @return string
928
     */
929
    protected function xml_header($charsetEncoding = '')
930
    {
931
        if ($charsetEncoding != '') {
932
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
933 22
        } else {
934
            return "<?xml version=\"1.0\"?" . ">\n";
935 22
        }
936 22
    }
937 22
938
    /**
939 22
     * @param string $methName
940 22
     * @return bool
941
     */
942
    protected function isSyscall($methName)
943 22
    {
944
        return (strpos($methName, "system.") === 0);
945
    }
946
947
948
    /**
949
     * @return array[]
950
     */
951 106
    public function getDispatchMap()
952
    {
953
        return $this->dmap;
954 106
    }
955 106
956 106
    /**
957
     * @return array[]
958
     */
959
    public function getSystemDispatchMap()
960 106
    {
961 85
        if (!$this->allow_system_funcs) {
962
            return array();
963 22
        }
964
965 106
        return array(
966 106
            'system.listMethods' => array(
967 106
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
968 106
                // listMethods: signature was either a string, or nothing.
969 106
                // The useless string variant has been removed
970 106
                'signature' => array(array(Value::$xmlrpcArray)),
971 106
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
972
                'signature_docs' => array(array('list of method names')),
973 106
            ),
974
            'system.methodHelp' => array(
975 106
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
976
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
977
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
978
                'signature_docs' => array(array('method description', 'name of the method to be described')),
979
            ),
980
            'system.methodSignature' => array(
981
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
982 1
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
983
                '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)',
984
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
985 106
            ),
986
            'system.multicall' => array(
987
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
988
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
989
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
990
                '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"')),
991
            ),
992
            'system.getCapabilities' => array(
993 85
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
994
                'signature' => array(array(Value::$xmlrpcStruct)),
995
                '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',
996 85
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
997 85
            ),
998 85
        );
999
    }
1000
1001
    /**
1002 85
     * @return array[]
1003 85
     */
1004
    public function getCapabilities()
1005 1
    {
1006
        $outAr = array(
1007 85
            // xmlrpc spec: always supported
1008 85
            'xmlrpc' => array(
1009 85
                'specUrl' => 'http://www.xmlrpc.com/spec',
1010
                'specVersion' => 1
1011
            ),
1012
            // if we support system.xxx functions, we always support multicall, too...
1013
            // Note that, as of 2006/09/17, the following URL does not respond anymore
1014
            'system.multicall' => array(
1015
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
1016
                'specVersion' => 1
1017 85
            ),
1018
            // introspection: version 2! we support 'mixed', too
1019
            'introspection' => array(
1020 64
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
1021
                'specVersion' => 2,
1022 64
            ),
1023 64
        );
1024 64
1025
        // NIL extension
1026 64
        if (PhpXmlRpc::$xmlrpc_null_extension) {
1027 64
            $outAr['nil'] = array(
1028
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
1029 64
                'specVersion' => 1
1030 64
            );
1031 64
        }
1032
1033 64
        return $outAr;
1034
    }
1035
1036
    /**
1037
     * @internal handler of a system. method
1038
     *
1039
     * @param Server $server
1040
     * @param Request $req
1041 64
     * @return Response
1042
     */
1043 64
    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

1043
    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...
1044
    {
1045
        $encoder = new Encoder();
1046 64
        return new Response($encoder->encode($server->getCapabilities()));
1047 64
    }
1048
1049
    /**
1050 64
     * @internal handler of a system. method
1051
     *
1052
     * @param Server $server
1053 64
     * @param Request $req if called in plain php values mode, second param is missing
1054 64
     * @return Response
1055
     */
1056
    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

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