Passed
Push — master ( b81870...2e14d2 )
by Gaetano
08:05
created

Server::__construct()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 11
c 1
b 0
f 0
nc 24
nop 2
dl 0
loc 25
ccs 4
cts 4
cp 1
crap 7
rs 8.8333
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. Instead, you can use the class add_to_map() function
200
        // to add functions manually (borrowed from SOAPX4)
201
        if ($dispatchMap) {
202
            $this->setDispatchMap($dispatchMap);
203
            if ($serviceNow) {
204 2
                $this->service();
205
            }
206 2
        }
207 2
    }
208
209
    /**
210
     * Set debug level of server.
211
     *
212
     * @param integer $level debug lvl: determines info added to xmlrpc responses (as xml comments)
213
     *                    0 = no debug info,
214
     *                    1 = msgs set from user with debugmsg(),
215
     *                    2 = add complete xmlrpc request (headers and body),
216 22
     *                    3 = add also all processing warnings happened during method processing
217
     *                    (NB: this involves setting a custom error handler, and might interfere
218 22
     *                    with the standard processing of the php function exposed as method. In
219 22
     *                    particular, triggering an USER_ERROR level error will not halt script
220
     *                    execution anymore, but just end up logged in the xmlrpc response)
221
     *                    Note that info added at level 2 and 3 will be base64 encoded
222
     * @return $this
223
     */
224
    public function setDebug($level)
225
    {
226
        $this->debug = $level;
227
        return $this;
228 561
    }
229
230
    /**
231
     * Add a string to the debug info that can be later serialized by the server as part of the response message.
232
     * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
233
     * character set.
234
     *
235 561
     * @param string $msg
236 561
     * @return void
237 559
     */
238
    public static function xmlrpc_debugmsg($msg)
239 561
    {
240 2
        static::$_xmlrpc_debuginfo .= $msg . "\n";
241
    }
242
243
    /**
244
     * Add a string to the debug info that will be later serialized by the server as part of the response message
245
     * (base64 encoded, only when debug level >= 2)
246 561
     *
247
     * @param string $msg
248
     * @return void
249
     */
250
    public static function error_occurred($msg)
251
    {
252
        static::$_xmlrpcs_occurred_errors .= $msg . "\n";
253
    }
254
255
    /**
256
     * Return a string with the serialized representation of all debug info.
257
     *
258
     * @internal this function will become protected in the future
259 561
     *
260
     * @param string $charsetEncoding the target charset encoding for the serialization
261 561
     *
262 561
     * @return string an XML comment (or two)
263
     */
264 561
    public function serializeDebug($charsetEncoding = '')
265
    {
266
        // Tough encoding problem: which internal charset should we assume for debug info?
267 561
        // It might contain a copy of raw data received from client, ie with unknown encoding,
268
        // intermixed with php generated data and user generated data...
269
        // so we split it: system debug is base 64 encoded,
270 561
        // user debug info should be encoded by the end user using the INTERNAL_ENCODING
271 559
        $out = '';
272
        if ($this->debug_info != '') {
273
            $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
274 561
        }
275 561
        if (static::$_xmlrpc_debuginfo != '') {
276
            $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
277 561
            // NB: a better solution MIGHT be to use CDATA, but we need to insert it
278
            // into return payload AFTER the beginning tag
279
            //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
280
        }
281
282 561
        return $out;
283
    }
284
285 561
    /**
286 22
     * Execute the xmlrpc request, printing the response.
287 22
     *
288
     * @param string $data the request body. If null, the http POST request will be examined
289
     * @param bool $returnPayload When true, return the response but do not echo it or any http header
290 561
     *
291 561
     * @return Response|string the response object (usually not used by caller...) or its xml serialization
292 561
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
293
     */
294
    public function service($data = null, $returnPayload = false)
295
    {
296 561
        if ($data === null) {
297 561
            $data = file_get_contents('php://input');
298
        }
299 561
        $rawData = $data;
300
301 561
        // reset internal debug info
302
        $this->debug_info = '';
303
304
        // Save what we received, before parsing it
305
        if ($this->debug > 1) {
306
            $this->debugmsg("+++GOT+++\n" . $data . "\n+++END+++");
307 561
        }
308 561
309
        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
310
        if (!$resp) {
311 561
            // this actually executes the request
312
            $resp = $this->parseRequest($data, $reqCharset);
313
314
            // save full body of request into response, for debugging purposes.
315
            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
316
            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
317 561
            ///       better: just avoid setting this, and set debug info of the received http request in the request
318 561
            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
319 104
            ///       Last but not least: the raw data might be of use to handler functions - but in decompressed form...
320
            $resp->raw_data = $rawData;
321 104
        }
322 52
323 52
        if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
324 52
            $this->debugmsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
325 52
                static::$_xmlrpcs_occurred_errors . "+++END+++");
326 52
        }
327 52
328 52
        $payload = $this->xml_header($respCharset);
329
        if ($this->debug > 0) {
330
            $payload = $payload . $this->serializeDebug($respCharset);
331
        }
332
333
        // Do not create response serialization if it has already happened. Helps to build json magic
334
        /// @todo what if the payload was created targeting a different charset than $respCharset?
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 with the encodings we know we can generate.
535
                /// @todo we should parse q=0.x preferences instead of getting first charset specified...
536
                $clientAcceptedCharsets = explode(',', strtoupper($_SERVER['HTTP_ACCEPT_CHARSET']));
537
                // Give preference to internal encoding
538
/// @todo if mbstring is enabled, we can support other charsets too! Add a method to the Encoder!
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
        /// @todo move this block of code into the XMLParser
587
        if ($reqEncoding != '') {
588 2
            // Since parsing will fail if
589 2
            // - charset is not specified in the xml declaration,
590 2
            // - the encoding is not UTF8 and
591 2
            // - there are non-ascii chars in the text,
592 560
            // we try to work round that...
593 1
            // The following code might be better for mb_string enabled installs, but it 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__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
603
                    }
604
                }
605
            }
606
        }
607
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
608
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, ie. utf8
609
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
610
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
611
        } else {
612 559
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
613
        }
614 559
        // register a callback with the xml parser for when it finds the method name
615 538
        $options['methodname_callback'] = array($this, 'methodNameCallback');
616
617
        $xmlRpcParser = $this->getParser();
618 559
        try {
619 559
            $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
620
        } catch (PhpXmlrpcException $e) {
621 559
            return new Response(0, $e->getCode(), $e->getMessage());
622
        }
623
624
        if ($xmlRpcParser->_xh['isf'] == 3) {
625 562
            // (BC) we return XML error as a faultCode
626
            preg_match('/^XML error ([0-9]+)/', $xmlRpcParser->_xh['isf_reason'], $matches);
627
            return new Response(
628
                0,
629
                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
630
                $xmlRpcParser->_xh['isf_reason']);
631
        } elseif ($xmlRpcParser->_xh['isf']) {
632
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc,
633
            ///       parsing error
634
            return new Response(
635
                0,
636
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
637
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
638
        } else {
639 559
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
640
            // this, but in the most common scenario (xmlrpc values type server with some methods registered as phpvals)
641 559
            // that would mean a useless encode+decode pass
642 559
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
643
                (isset($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type']) &&
644 559
                    ($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type'] != 'xmlrpcvals')
645 559
                )
646
            ) {
647
                if ($this->debug > 1) {
648
                    $this->debugmsg("\n+++PARSED+++\n" . var_export($xmlRpcParser->_xh['params'], true) . "\n+++END+++");
649 559
                }
650 559
651
                return $this->execute($xmlRpcParser->_xh['method'], $xmlRpcParser->_xh['params'], $xmlRpcParser->_xh['pt']);
652 559
            } else {
653
                // build a Request object with data parsed from xml and add parameters in
654 85
                $req = new Request($xmlRpcParser->_xh['method']);
655 85
                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...
656 85
                    $req->addParam($xmlRpcParser->_xh['params'][$i]);
657
                }
658
659
                if ($this->debug > 1) {
660 559
                    $this->debugmsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
661 536
                }
662 536
663 536
                return $this->execute($req);
664
            }
665
        }
666
    }
667 536
668
    /**
669 22
     * Execute a method invoked by the client, checking parameters used.
670 22
     *
671 22
     * @param Request|string $req either a Request obj or a method name
672 22
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
673
     * @param string[] $paramTypes array with xmlrpc types of method parameters (only if $req is method name)
674
     * @return Response
675
     *
676
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
677 559
     */
678
    protected function execute($req, $params = null, $paramTypes = null)
679 559
    {
680 127
        static::$_xmlrpcs_occurred_errors = '';
681
        static::$_xmlrpc_debuginfo = '';
682
683 559
        if (is_object($req)) {
684 150
            $methodName = $req->method();
685 24
        } else {
686
            $methodName = $req;
687 127
        }
688
689 431
        $sysCall = $this->isSyscall($methodName);
690 129
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
691
692 324
        if (!isset($dmap[$methodName]['function'])) {
693
            // No such method
694
            return new Response(0,
695
                PhpXmlRpc::$xmlrpcerr['unknown_method'],
696 559
                PhpXmlRpc::$xmlrpcstr['unknown_method']);
697
        }
698
699
        // Check signature
700
        if (isset($dmap[$methodName]['signature'])) {
701
            $sig = $dmap[$methodName]['signature'];
702
            if (is_object($req)) {
703
                list($ok, $errStr) = $this->verifySignature($req, $sig);
704
            } else {
705
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
706
            }
707 559
            if (!$ok) {
708 559
                // Didn't match.
709
                return new Response(
710
                    0,
711
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
712
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
713 559
                );
714 559
            }
715 127
        }
716
717 454
        $func = $dmap[$methodName]['function'];
718
719 557
        // let the 'class::function' syntax be accepted in dispatch maps
720
        if (is_string($func) && strpos($func, '::')) {
721
            $func = explode('::', $func);
722
        }
723
724
        // build string representation of function 'name'
725
        if (is_array($func)) {
726
            if (is_object($func[0])) {
727
                $funcName = get_class($func[0]) . '->' . $func[1];
728
            } else {
729
                $funcName = implode('::', $func);
730
            }
731
        } else if ($func instanceof \Closure) {
732
            $funcName = 'Closure';
733
        } else {
734
            $funcName = $func;
735
        }
736
737
        // verify that function to be invoked is in fact callable
738
        if (!is_callable($func)) {
739
            $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
740
            return new Response(
741
                0,
742
                PhpXmlRpc::$xmlrpcerr['server_error'],
743
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
744
            );
745
        }
746
747
        // If debug level is 3, we should catch all errors generated during processing of user function, and log them
748
        // as part of response
749
        if ($this->debug > 2) {
750
            self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
751
        }
752
753
        try {
754
            // Allow mixed-convention servers
755
            if (is_object($req)) {
756
                // call an 'xml-rpc aware' function
757
                if ($sysCall) {
758
                    $r = call_user_func($func, $this, $req);
759
                } else {
760
                    $r = call_user_func($func, $req);
761
                }
762 45
                if (!is_a($r, 'PhpXmlRpc\Response')) {
763
                    $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
764
                    if (is_a($r, 'PhpXmlRpc\Value')) {
765 45
                        $r = new Response($r);
766 45
                    } else {
767
                        $r = new Response(
768
                            0,
769
                            PhpXmlRpc::$xmlrpcerr['server_error'],
770
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
771
                        );
772
                    }
773
                }
774
            } else {
775 45
                // call a 'plain php' function
776 2
                if ($sysCall) {
777 2
                    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

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

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

888
            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

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

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

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