Passed
Push — master ( 6f0f8c...d68788 )
by Gaetano
06:26
created

Server::methodNameCallback()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

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

858
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
859
                    $r = call_user_func_array($func, $params);
860 127
                } else {
861 127
                    // 3rd API convention for method-handling functions: EPI-style
862 127
                    if ($this->functions_parameters_type == 'epivals') {
863
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
864
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
865
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
866 127
                            $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
867 127
                        } else {
868 127
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
869
                            // return type correctly
870
                            $encoder = new Encoder();
871
                            $r = new Response($encoder->encode($r, array('extension_api')));
872 127
                        }
873 127
                    } else {
874 127
                        $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

874
                        $r = call_user_func_array($func, /** @scrutinizer ignore-type */ $params);
Loading history...
875
                    }
876
                }
877
                // the return type can be either a Response object or a plain php value...
878
                if (!is_a($r, '\PhpXmlRpc\Response')) {
879
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
880
                    // a: let the user decide
881
                    $encoder = new Encoder();
882
                    $r = new Response($encoder->encode($r, $this->phpvals_encoding_options));
883
                }
884
            }
885
        /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
886
        } catch (\Exception $e) {
887
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
888
            // proper error-response
889
            switch ($exception_handling) {
890
                case 2:
891
                    if ($this->debug > 2) {
892
                        if (self::$_xmlrpcs_prev_ehandler) {
893
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
894
                        } else {
895
                            restore_error_handler();
896
                        }
897
                    }
898
                    throw $e;
899
                case 1:
900
                    $errCode = $e->getCode();
901
                    if ($errCode == 0) {
902
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
903
                    }
904
                    $r = new Response(0, $errCode, $e->getMessage());
905
                    break;
906
                default:
907
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
908
            }
909
        } catch (\Error $e) {
910
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
911
            // proper error-response
912
            switch ($exception_handling) {
913
                case 2:
914
                    if ($this->debug > 2) {
915
                        if (self::$_xmlrpcs_prev_ehandler) {
916
                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
917
                        } else {
918
                            restore_error_handler();
919
                        }
920
                    }
921
                    throw $e;
922
                case 1:
923
                    $errCode = $e->getCode();
924
                    if ($errCode == 0) {
925
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
926
                    }
927
                    $r = new Response(0, $errCode, $e->getMessage());
928
                    break;
929
                default:
930
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
931
            }
932
        }
933 22
934
        if ($this->debug > 2) {
935 22
            // note: restore the error handler we found before calling the user func, even if it has been changed
936 22
            // inside the func itself
937 22
            if (self::$_xmlrpcs_prev_ehandler) {
938
                set_error_handler(self::$_xmlrpcs_prev_ehandler);
939 22
            } else {
940 22
                restore_error_handler();
941
            }
942
        }
943 22
944
        return $r;
945
    }
946
947
    /**
948
     * Registered as callback for when the XMLParser has found the name of the method to execute.
949
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
950
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
951 106
     *
952
     * @internal
953
     * @param $methodName
954 106
     * @param XMLParser $xmlParser
955 106
     * @param resource $parser
956 106
     * @return void
957
     * @throws NoSuchMethodException
958
     *
959
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
960 106
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
961 85
     *       about the matched method handler, in order to avoid doing the work twice...
962
     */
963 22
    public function methodNameCallback($methodName, $xmlParser, $parser)
964
    {
965 106
        $sysCall = $this->isSyscall($methodName);
966 106
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
967 106
968 106
        if (!isset($dmap[$methodName]['function'])) {
969 106
            // No such method
970 106
            throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
971 106
        }
972
973 106
        // alter on-the-fly the config of the xml parser if needed
974
        if (isset($dmap[$methodName]['parameters_type']) &&
975 106
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
976
            /// @todo this should be done by a method of the XMLParser
977
            switch ($dmap[$methodName]['parameters_type']) {
978
                case XMLParser::RETURN_PHP:
979
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
980
                    break;
981
                case XMLParser::RETURN_EPIVALS:
982 1
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
983
                    break;
984
                /// @todo log a warning on unsupported return type
985 106
                case XMLParser::RETURN_XMLRPCVALS:
986
                default:
987
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
988
            }
989
        }
990
    }
991
992
    /**
993 85
     * Add a string to the 'internal debug message' (separate from 'user debug message').
994
     *
995
     * @param string $string
996 85
     * @return void
997 85
     */
998 85
    protected function debugmsg($string)
999
    {
1000
        $this->debug_info .= $string . "\n";
1001
    }
1002 85
1003 85
    /**
1004
     * @param string $charsetEncoding
1005 1
     * @return string
1006
     */
1007 85
    protected function xml_header($charsetEncoding = '')
1008 85
    {
1009 85
        if ($charsetEncoding != '') {
1010
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
1011
        } else {
1012
            return "<?xml version=\"1.0\"?" . ">\n";
1013
        }
1014
    }
1015
1016
    /**
1017 85
     * @param string $methName
1018
     * @return bool
1019
     */
1020 64
    protected function isSyscall($methName)
1021
    {
1022 64
        return (strpos($methName, "system.") === 0);
1023 64
    }
1024 64
1025
1026 64
    /**
1027 64
     * @param array $dmap
1028
     * @return $this
1029 64
     */
1030 64
    public function setDispatchMap($dmap)
1031 64
    {
1032
        $this->dmap = $dmap;
1033 64
        return $this;
1034
    }
1035
1036
    /**
1037
     * @return array[]
1038
     */
1039
    public function getDispatchMap()
1040
    {
1041 64
        return $this->dmap;
1042
    }
1043 64
1044
    /**
1045
     * @return array[]
1046 64
     */
1047 64
    public function getSystemDispatchMap()
1048
    {
1049
        if (!$this->allow_system_funcs) {
1050 64
            return array();
1051
        }
1052
1053 64
        return array(
1054 64
            'system.listMethods' => array(
1055
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
1056
                // listMethods: signature was either a string, or nothing.
1057 64
                // The useless string variant has been removed
1058 64
                'signature' => array(array(Value::$xmlrpcArray)),
1059
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
1060
                'signature_docs' => array(array('list of method names')),
1061 64
            ),
1062
            'system.methodHelp' => array(
1063
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
1064
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
1065 64
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
1066 64
                'signature_docs' => array(array('method description', 'name of the method to be described')),
1067 64
            ),
1068
            'system.methodSignature' => array(
1069
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
1070
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
1071
                '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)',
1072
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
1073
            ),
1074
            'system.multicall' => array(
1075 64
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
1076
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
1077 64
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
1078 64
                '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"')),
1079
            ),
1080
            'system.getCapabilities' => array(
1081 64
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
1082
                'signature' => array(array(Value::$xmlrpcStruct)),
1083
                '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',
1084
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
1085
            ),
1086
        );
1087
    }
1088
1089
    /**
1090
     * @return array[]
1091
     */
1092
    public function getCapabilities()
1093
    {
1094
        $outAr = array(
1095
            // xml-rpc spec: always supported
1096
            'xmlrpc' => array(
1097
                'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md
1098
                'specVersion' => 1
1099
            ),
1100
            // if we support system.xxx functions, we always support multicall, too...
1101
            'system.multicall' => array(
1102
                // Note that, as of 2006/09/17, the following URL does not respond anymore
1103
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
1104
                'specVersion' => 1
1105
            ),
1106
            // introspection: version 2! we support 'mixed', too.
1107
            // note: the php xml-rpc extension says this instead:
1108
            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
1109
            'introspection' => array(
1110
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
1111
                'specVersion' => 2,
1112
            ),
1113
        );
1114
1115
        // NIL extension
1116
        if (PhpXmlRpc::$xmlrpc_null_extension) {
1117
            $outAr['nil'] = array(
1118
                // Note that, as of 2023/01, the following URL does not respond anymore
1119
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
1120
                'specVersion' => 1
1121
            );
1122
        }
1123
1124
        // support for "standard" error codes
1125
        if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
1126
            $outAr['faults_interop'] = array(
1127
                'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
1128
                'specVersion' => 20010516
1129
            );
1130
        }
1131
1132
        return $outAr;
1133
    }
1134
1135
    /**
1136
     * @internal handler of a system. method
1137 85
     *
1138
     * @param Server $server
1139 85
     * @param Request $req
1140
     * @return Response
1141 85
     */
1142 85
    public static function _xmlrpcs_getCapabilities($server, $req = null)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed. ( Ignorable by Annotation )

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

1142
    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...
1143 85
    {
1144 64
        $encoder = new Encoder();
1145
        return new Response($encoder->encode($server->getCapabilities()));
1146
    }
1147
1148
    /**
1149
     * @internal handler of a system. method
1150
     *
1151
     * @param Server $server
1152
     * @param Request $req if called in plain php values mode, second param is missing
1153 85
     * @return Response
1154
     */
1155
    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

1155
    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...
1156
    {
1157
        $outAr = array();
1158
        foreach ($server->dmap as $key => $val) {
1159
            $outAr[] = new Value($key, 'string');
1160
        }
1161
        foreach ($server->getSystemDispatchMap() as $key => $val) {
1162
            $outAr[] = new Value($key, 'string');
1163
        }
1164 43
1165
        return new Response(new Value($outAr, 'array'));
1166
    }
1167 43
1168 22
    /**
1169
     * @internal handler of a system. method
1170
     *
1171
     * @param Server $server
1172 22
     * @param Request $req
1173 22
     * @return Response
1174
     */
1175
    public static function _xmlrpcs_methodSignature($server, $req)
1176
    {
1177 22
        // let's accept as parameter either an xml-rpc value or string
1178
        if (is_object($req)) {
1179
            $methName = $req->getParam(0);
1180 22
            $methName = $methName->scalarVal();
1181 22
        } else {
1182 22
            $methName = $req;
1183
        }
1184 22
        if ($server->isSyscall($methName)) {
1185
            $dmap = $server->getSystemDispatchMap();
1186
        } else {
1187
            $dmap = $server->dmap;
1188
        }
1189
        if (isset($dmap[$methName])) {
1190
            if (isset($dmap[$methName]['signature'])) {
1191
                $sigs = array();
1192
                foreach ($dmap[$methName]['signature'] as $inSig) {
1193
                    $curSig = array();
1194
                    foreach ($inSig as $sig) {
1195
                        $curSig[] = new Value($sig, 'string');
1196
                    }
1197
                    $sigs[] = new Value($curSig, 'array');
1198 22
                }
1199
                $r = new Response(new Value($sigs, 'array'));
1200
            } else {
1201
                // NB: according to the official docs, we should be returning a
1202
                // "none-array" here, which means not-an-array
1203
                $r = new Response(new Value('undef', 'string'));
1204
            }
1205
        } else {
1206
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1207
        }
1208
1209
        return $r;
1210
    }
1211
1212
    /**
1213
     * @internal handler of a system. method
1214
     *
1215
     * @param Server $server
1216
     * @param Request $req
1217
     * @return Response
1218
     */
1219
    public static function _xmlrpcs_methodHelp($server, $req)
1220
    {
1221
        // let's accept as parameter either an xml-rpc value or string
1222
        if (is_object($req)) {
1223
            $methName = $req->getParam(0);
1224
            $methName = $methName->scalarVal();
1225
        } else {
1226
            $methName = $req;
1227
        }
1228
        if ($server->isSyscall($methName)) {
1229
            $dmap = $server->getSystemDispatchMap();
1230
        } else {
1231
            $dmap = $server->dmap;
1232
        }
1233
        if (isset($dmap[$methName])) {
1234
            if (isset($dmap[$methName]['docstring'])) {
1235
                $r = new Response(new Value($dmap[$methName]['docstring'], 'string'));
1236
            } else {
1237
                $r = new Response(new Value('', 'string'));
1238
            }
1239
        } else {
1240
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1241
        }
1242
1243
        return $r;
1244
    }
1245
1246
    /**
1247
     * @internal this function will become protected in the future
1248
     *
1249
     * @param $err
1250
     * @return Value
1251
     */
1252
    public static function _xmlrpcs_multicall_error($err)
1253
    {
1254
        if (is_string($err)) {
1255
            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
1256
            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
1257
        } else {
1258
            $code = $err->faultCode();
1259
            $str = $err->faultString();
1260
        }
1261
        $struct = array();
1262
        $struct['faultCode'] = new Value($code, 'int');
1263
        $struct['faultString'] = new Value($str, 'string');
1264
1265
        return new Value($struct, 'struct');
1266
    }
1267
1268
    /**
1269
     * @internal this function will become protected in the future
1270
     *
1271
     * @param Server $server
1272
     * @param Value $call
1273
     * @return Value
1274
     */
1275
    public static function _xmlrpcs_multicall_do_call($server, $call)
1276
    {
1277
        if ($call->kindOf() != 'struct') {
1278
            return static::_xmlrpcs_multicall_error('notstruct');
1279
        }
1280
        $methName = @$call['methodName'];
1281
        if (!$methName) {
1282
            return static::_xmlrpcs_multicall_error('nomethod');
1283
        }
1284
        if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
1285
            return static::_xmlrpcs_multicall_error('notstring');
1286
        }
1287
        if ($methName->scalarVal() == 'system.multicall') {
1288
            return static::_xmlrpcs_multicall_error('recursion');
1289
        }
1290
1291
        $params = @$call['params'];
1292
        if (!$params) {
1293
            return static::_xmlrpcs_multicall_error('noparams');
1294
        }
1295
        if ($params->kindOf() != 'array') {
1296
            return static::_xmlrpcs_multicall_error('notarray');
1297
        }
1298
1299
        $req = new Request($methName->scalarVal());
1300
        foreach ($params as $i => $param) {
1301
            if (!$req->addParam($param)) {
1302
                $i++; // for error message, we count params from 1
1303
                return static::_xmlrpcs_multicall_error(new Response(0,
1304
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1305
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1306
            }
1307
        }
1308
1309
        $result = $server->execute($req);
1310
1311
        if ($result->faultCode() != 0) {
1312
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1313
        }
1314
1315
        return new Value(array($result->value()), 'array');
1316
    }
1317
1318
    /**
1319
     * @internal this function will become protected in the future
1320
     *
1321
     * @param Server $server
1322
     * @param Value $call
1323
     * @return Value
1324
     */
1325
    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1326
    {
1327
        if (!is_array($call)) {
0 ignored issues
show
introduced by
The condition is_array($call) is always false.
Loading history...
1328
            return static::_xmlrpcs_multicall_error('notstruct');
1329
        }
1330
        if (!array_key_exists('methodName', $call)) {
1331
            return static::_xmlrpcs_multicall_error('nomethod');
1332
        }
1333
        if (!is_string($call['methodName'])) {
1334
            return static::_xmlrpcs_multicall_error('notstring');
1335
        }
1336
        if ($call['methodName'] == 'system.multicall') {
1337
            return static::_xmlrpcs_multicall_error('recursion');
1338
        }
1339
        if (!array_key_exists('params', $call)) {
1340
            return static::_xmlrpcs_multicall_error('noparams');
1341
        }
1342
        if (!is_array($call['params'])) {
1343
            return static::_xmlrpcs_multicall_error('notarray');
1344
        }
1345
1346
        // this is a simplistic hack, since we might have received
1347
        // base64 or datetime values, but they will be listed as strings here...
1348
        $pt = array();
1349
        $wrapper = new Wrapper();
1350
        foreach ($call['params'] as $val) {
1351
            // support EPI-encoded base64 and datetime values
1352
            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
1353
                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
1354
            } else {
1355
                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
1356
            }
1357
        }
1358
1359
        $result = $server->execute($call['methodName'], $call['params'], $pt);
1360
1361
        if ($result->faultCode() != 0) {
1362
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1363
        }
1364
1365
        return new Value(array($result->value()), 'array');
1366
    }
1367
1368
    /**
1369
     * @internal handler of a system. method
1370
     *
1371
     * @param Server $server
1372
     * @param Request|array $req
1373
     * @return Response
1374
     */
1375
    public static function _xmlrpcs_multicall($server, $req)
1376
    {
1377
        $result = array();
1378
        // let accept a plain list of php parameters, beside a single xml-rpc msg object
1379
        if (is_object($req)) {
1380
            $calls = $req->getParam(0);
1381
            foreach ($calls as $call) {
1382
                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
1383
            }
1384
        } else {
1385
            $numCalls = count($req);
1386
            for ($i = 0; $i < $numCalls; $i++) {
1387
                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
1388
            }
1389
        }
1390
1391
        return new Response(new Value($result, 'array'));
1392
    }
1393
1394
    /**
1395
     * Error handler used to track errors that occur during server-side execution of PHP code.
1396
     * This allows to report back to the client whether an internal error has occurred or not
1397
     * using an xml-rpc response object, instead of letting the client deal with the html junk
1398
     * that a PHP execution error on the server generally entails.
1399
     *
1400
     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
1401
     *
1402
     * @internal
1403
     */
1404
    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
1405
    {
1406
        // obey the @ protocol
1407
        if (error_reporting() == 0) {
1408
            return;
1409
        }
1410
1411
        //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
1412
        if ($errCode != E_STRICT) {
1413
            static::error_occurred($errString);
1414
        }
1415
1416
        // Try to avoid as much as possible disruption to the previous error handling mechanism in place
1417
        if (self::$_xmlrpcs_prev_ehandler == '') {
1418
            // The previous error handler was the default: all we should do is log error to the default error log
1419
            // (if level high enough)
1420
            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
1421
                // we can't use the functionality of LoggerAware, because this is a static method
1422
                if (self::$logger === null) {
1423
                    self::$logger = Logger::instance();
1424
                }
1425
                self::$logger->error($errString);
1426
            }
1427
        } else {
1428
            // Pass control on to previous error handler, trying to avoid loops...
1429
            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...
1430
                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...
1431
                    // the following works both with static class methods and plain object methods as error handler
1432
                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
1433
                } else {
1434
                    $method = self::$_xmlrpcs_prev_ehandler;
1435
                    $method($errCode, $errString, $filename, $lineNo, $context);
1436
                }
1437
            }
1438
        }
1439
    }
1440
}
1441