Passed
Push — master ( ebef44...752d49 )
by Gaetano
05:28
created

Server::__get()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 17
nc 9
nop 1
dl 0
loc 19
ccs 0
cts 0
cp 0
crap 90
rs 8.0555
c 0
b 0
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\DeprecationLogger;
13
use PhpXmlRpc\Traits\ParserAware;
14
15
/**
16
 * Allows effortless implementation of XML-RPC servers
17
 *
18
 * @property string[] $accepted_compression deprecated - public access left in purely for BC. Access via getOption()/setOption()
19
 * @property bool $allow_system_funcs deprecated - public access left in purely for BC. Access via getOption()/setOption()
20
 * @property bool $compress_response deprecated - public access left in purely for BC. Access via getOption()/setOption()
21
 * @property int $debug deprecated - public access left in purely for BC. Access via getOption()/setOption()
22
 * @property int $exception_handling deprecated - public access left in purely for BC. Access via getOption()/setOption()
23
 * @property string $functions_parameters_type deprecated - public access left in purely for BC. Access via getOption()/setOption()
24
 * @property array $phpvals_encoding_options deprecated - public access left in purely for BC. Access via getOption()/setOption()
25
 * @property string $response_charset_encoding deprecated - public access left in purely for BC. Access via getOption()/setOption()
26
 */
27
class Server
28
{
29
    use CharsetEncoderAware;
30
    use DeprecationLogger;
31
    use ParserAware;
32
33
    const OPT_ACCEPTED_COMPRESSION = 'accepted_compression';
34
    const OPT_ALLOW_SYSTEM_FUNCS = 'allow_system_funcs';
35
    const OPT_COMPRESS_RESPONSE = 'compress_response';
36
    const OPT_DEBUG = 'debug';
37
    const OPT_EXCEPTION_HANDLING = 'exception_handling';
38
    const OPT_FUNCTIONS_PARAMETERS_TYPE = 'functions_parameters_type';
39
    const OPT_PHPVALS_ENCODING_OPTIONS = 'phpvals_encoding_options';
40
    const OPT_RESPONSE_CHARSET_ENCODING = 'response_charset_encoding';
41
42
    /**
43
     * @var string
44
     * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
45
     * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc).
46
     *
47
     * @todo create class constants for these
48
     */
49
    protected $functions_parameters_type = 'xmlrpcvals';
50
51
    /**
52
     * @var array
53
     * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map
54
     * when the functions_parameters_type member is set to 'phpvals'.
55
     * @see Encoder::encode for a list of values
56
     */
57
    protected $phpvals_encoding_options = array('auto_dates');
58
59
    /**
60
     * @var int
61
     * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
62
     * SECURITY SENSITIVE!
63
     * Valid values:
64
     * 0 =
65
     * 1 =
66
     * 2 =
67
     * 3 =
68
     */
69
    protected $debug = 1;
70
71
    /**
72
     * @var int
73
     * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
74
     * 0 = catch it and return an 'internal error' xml-rpc response (default)
75
     * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error
76
     *     corresponding to the exception, both its code and message.
77
     * 2 = allow the exception to float to the upper layers
78
     * Can be overridden per-method-handler in the dispatch map
79
     */
80
    protected $exception_handling = 0;
81
82
    /**
83
     * @var bool
84
     * When set to true, it will enable HTTP compression of the response, in case the client has declared its support
85
     * for compression in the request.
86
     * Automatically set at constructor time.
87
     */
88
    protected $compress_response = false;
89
90
    /**
91
     * @var string[]
92
     * List of http compression methods accepted by the server for requests. Automatically set at constructor time.
93
     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
94
     */
95
    protected $accepted_compression = array();
96
97
    /**
98
     * @var bool
99
     * Shall we serve calls to system.* methods?
100
     */
101
    protected $allow_system_funcs = true;
102
103
    /**
104
     * List of charset encodings natively accepted for requests.
105
     * Set at constructor time.
106
     * @deprecated UNUSED so far...
107
     */
108
    protected $accepted_charset_encodings = array();
109
110
    /**
111
     * @var string
112
     * Charset encoding to be used for response.
113
     * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
114
     * Can be:
115 562
     * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled),
116
     * - null (leave unspecified in response, convert output stream to US_ASCII),
117 562
     * - '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).
118 562
     * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
119
     */
120 562
    protected $response_charset_encoding = '';
121
122
    protected static $options = array(
123
        self::OPT_ACCEPTED_COMPRESSION,
124
        self::OPT_ALLOW_SYSTEM_FUNCS,
125
        self::OPT_COMPRESS_RESPONSE,
126
        self::OPT_DEBUG,
127
        self::OPT_EXCEPTION_HANDLING,
128 2
        self::OPT_FUNCTIONS_PARAMETERS_TYPE,
129
        self::OPT_PHPVALS_ENCODING_OPTIONS,
130 2
        self::OPT_RESPONSE_CHARSET_ENCODING,
131 2
    );
132
133 2
    /**
134
     * @var mixed
135
     * Extra data passed at runtime to method handling functions. Used only by EPI layer
136
     * @internal
137
     */
138
    public $user_data = null;
139
140
    /**
141
     * Array defining php functions exposed as xml-rpc methods by this server.
142
     * @var array[] $dmap
143
     */
144
    protected $dmap = array();
145
146
    /**
147
     * Storage for internal debug info.
148
     */
149
    protected $debug_info = '';
150
151
    protected static $_xmlrpc_debuginfo = '';
152 562
    protected static $_xmlrpcs_occurred_errors = '';
153
    protected static $_xmlrpcs_prev_ehandler = '';
154
155
    /**
156 562
     * @param array[] $dispatchMap the dispatch map with definition of exposed services
157 562
     *                             Array keys are the names of the method names.
158 562
     *                             Each array value is an array with the following members:
159
     *                             - function (callable)
160
     *                             - docstring (optional)
161
     *                             - signature (array, optional)
162 562
     *                             - signature_docs (array, optional)
163
     *                             - parameters_type (string, optional)
164
     *                             - exception_handling (int, optional)
165
     * @param boolean $serviceNow set to false in order to prevent the server from running upon construction
166
     */
167
    public function __construct($dispatchMap = null, $serviceNow = true)
168
    {
169
        // if ZLIB is enabled, let the server by default accept compressed requests,
170 562
        // and compress responses sent to clients that support them
171 561
        if (function_exists('gzinflate')) {
172 561
            $this->accepted_compression[] = 'gzip';
173 2
        }
174
        if (function_exists('gzuncompress')) {
175
            $this->accepted_compression[] = 'deflate';
176 562
        }
177
        if (function_exists('gzencode') || function_exists('gzcompress')) {
178
            $this->compress_response = true;
179
        }
180
181
        // by default the xml parser can support these 3 charset encodings
182
        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
0 ignored issues
show
Deprecated Code introduced by
The property PhpXmlRpc\Server::$accepted_charset_encodings has been deprecated: UNUSED so far... ( Ignorable by Annotation )

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

182
        /** @scrutinizer ignore-deprecated */ $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

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

846
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
847
                    $r = call_user_func_array($func, $params);
848
                } else {
849 127
                    // 3rd API convention for method-handling functions: EPI-style
850 127
                    if ($this->functions_parameters_type == 'epivals') {
851
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
852
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
853
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
854 127
                            $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
855 127
                        } else {
856 127
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
857
                            // return type correctly
858
                            $encoder = new Encoder();
859
                            $r = new Response($encoder->encode($r, array('extension_api')));
860 127
                        }
861 127
                    } else {
862 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

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

1116
    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...
1117
    {
1118
        $encoder = new Encoder();
1119
        return new Response($encoder->encode($server->getCapabilities()));
1120
    }
1121
1122
    /**
1123
     * @internal handler of a system. method
1124
     *
1125
     * @param Server $server
1126
     * @param Request $req if called in plain php values mode, second param is missing
1127
     * @return Response
1128
     */
1129
    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

1129
    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...
1130
    {
1131
        $outAr = array();
1132
        foreach ($server->dmap as $key => $val) {
1133
            $outAr[] = new Value($key, 'string');
1134
        }
1135
        foreach ($server->getSystemDispatchMap() as $key => $val) {
1136
            $outAr[] = new Value($key, 'string');
1137 85
        }
1138
1139 85
        return new Response(new Value($outAr, 'array'));
1140
    }
1141 85
1142 85
    /**
1143 85
     * @internal handler of a system. method
1144 64
     *
1145
     * @param Server $server
1146
     * @param Request $req
1147
     * @return Response
1148
     */
1149
    public static function _xmlrpcs_methodSignature($server, $req)
1150
    {
1151
        // let's accept as parameter either an xml-rpc value or string
1152
        if (is_object($req)) {
1153 85
            $methName = $req->getParam(0);
1154
            $methName = $methName->scalarVal();
1155
        } else {
1156
            $methName = $req;
1157
        }
1158
        if ($server->isSyscall($methName)) {
1159
            $dmap = $server->getSystemDispatchMap();
1160
        } else {
1161
            $dmap = $server->dmap;
1162
        }
1163
        if (isset($dmap[$methName])) {
1164 43
            if (isset($dmap[$methName]['signature'])) {
1165
                $sigs = array();
1166
                foreach ($dmap[$methName]['signature'] as $inSig) {
1167 43
                    $curSig = array();
1168 22
                    foreach ($inSig as $sig) {
1169
                        $curSig[] = new Value($sig, 'string');
1170
                    }
1171
                    $sigs[] = new Value($curSig, 'array');
1172 22
                }
1173 22
                $r = new Response(new Value($sigs, 'array'));
1174
            } else {
1175
                // NB: according to the official docs, we should be returning a
1176
                // "none-array" here, which means not-an-array
1177 22
                $r = new Response(new Value('undef', 'string'));
1178
            }
1179
        } else {
1180 22
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1181 22
        }
1182 22
1183
        return $r;
1184 22
    }
1185
1186
    /**
1187
     * @internal handler of a system. method
1188
     *
1189
     * @param Server $server
1190
     * @param Request $req
1191
     * @return Response
1192
     */
1193
    public static function _xmlrpcs_methodHelp($server, $req)
1194
    {
1195
        // let's accept as parameter either an xml-rpc value or string
1196
        if (is_object($req)) {
1197
            $methName = $req->getParam(0);
1198 22
            $methName = $methName->scalarVal();
1199
        } else {
1200
            $methName = $req;
1201
        }
1202
        if ($server->isSyscall($methName)) {
1203
            $dmap = $server->getSystemDispatchMap();
1204
        } else {
1205
            $dmap = $server->dmap;
1206
        }
1207
        if (isset($dmap[$methName])) {
1208
            if (isset($dmap[$methName]['docstring'])) {
1209
                $r = new Response(new Value($dmap[$methName]['docstring'], 'string'));
1210
            } else {
1211
                $r = new Response(new Value('', 'string'));
1212
            }
1213
        } else {
1214
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1215
        }
1216
1217
        return $r;
1218
    }
1219
1220
    /**
1221
     * @internal this function will become protected in the future
1222
     *
1223
     * @param $err
1224
     * @return Value
1225
     */
1226
    public static function _xmlrpcs_multicall_error($err)
1227
    {
1228
        if (is_string($err)) {
1229
            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
1230
            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
1231
        } else {
1232
            $code = $err->faultCode();
1233
            $str = $err->faultString();
1234
        }
1235
        $struct = array();
1236
        $struct['faultCode'] = new Value($code, 'int');
1237
        $struct['faultString'] = new Value($str, 'string');
1238
1239
        return new Value($struct, 'struct');
1240
    }
1241
1242
    /**
1243
     * @internal this function will become protected in the future
1244
     *
1245
     * @param Server $server
1246
     * @param Value $call
1247
     * @return Value
1248
     */
1249
    public static function _xmlrpcs_multicall_do_call($server, $call)
1250
    {
1251
        if ($call->kindOf() != 'struct') {
1252
            return static::_xmlrpcs_multicall_error('notstruct');
1253
        }
1254
        $methName = @$call['methodName'];
1255
        if (!$methName) {
1256
            return static::_xmlrpcs_multicall_error('nomethod');
1257
        }
1258
        if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
1259
            return static::_xmlrpcs_multicall_error('notstring');
1260
        }
1261
        if ($methName->scalarVal() == 'system.multicall') {
1262
            return static::_xmlrpcs_multicall_error('recursion');
1263
        }
1264
1265
        $params = @$call['params'];
1266
        if (!$params) {
1267
            return static::_xmlrpcs_multicall_error('noparams');
1268
        }
1269
        if ($params->kindOf() != 'array') {
1270
            return static::_xmlrpcs_multicall_error('notarray');
1271
        }
1272
1273
        $req = new Request($methName->scalarVal());
1274
        foreach ($params as $i => $param) {
1275
            if (!$req->addParam($param)) {
1276
                $i++; // for error message, we count params from 1
1277
                return static::_xmlrpcs_multicall_error(new Response(0,
1278
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1279
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1280
            }
1281
        }
1282
1283
        $result = $server->execute($req);
1284
1285
        if ($result->faultCode() != 0) {
1286
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1287
        }
1288
1289
        return new Value(array($result->value()), 'array');
1290
    }
1291
1292
    /**
1293
     * @internal this function will become protected in the future
1294
     *
1295
     * @param Server $server
1296
     * @param Value $call
1297
     * @return Value
1298
     */
1299
    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1300
    {
1301
        if (!is_array($call)) {
0 ignored issues
show
introduced by
The condition is_array($call) is always false.
Loading history...
1302
            return static::_xmlrpcs_multicall_error('notstruct');
1303
        }
1304
        if (!array_key_exists('methodName', $call)) {
1305
            return static::_xmlrpcs_multicall_error('nomethod');
1306
        }
1307
        if (!is_string($call['methodName'])) {
1308
            return static::_xmlrpcs_multicall_error('notstring');
1309
        }
1310
        if ($call['methodName'] == 'system.multicall') {
1311
            return static::_xmlrpcs_multicall_error('recursion');
1312
        }
1313
        if (!array_key_exists('params', $call)) {
1314
            return static::_xmlrpcs_multicall_error('noparams');
1315
        }
1316
        if (!is_array($call['params'])) {
1317
            return static::_xmlrpcs_multicall_error('notarray');
1318
        }
1319
1320
        // this is a simplistic hack, since we might have received
1321
        // base64 or datetime values, but they will be listed as strings here...
1322
        $pt = array();
1323
        $wrapper = new Wrapper();
1324
        foreach ($call['params'] as $val) {
1325
            // support EPI-encoded base64 and datetime values
1326
            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
1327
                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
1328
            } else {
1329
                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
1330
            }
1331
        }
1332
1333
        $result = $server->execute($call['methodName'], $call['params'], $pt);
1334
1335
        if ($result->faultCode() != 0) {
1336
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1337
        }
1338
1339
        return new Value(array($result->value()), 'array');
1340
    }
1341
1342
    /**
1343
     * @internal handler of a system. method
1344
     *
1345
     * @param Server $server
1346
     * @param Request|array $req
1347
     * @return Response
1348
     */
1349
    public static function _xmlrpcs_multicall($server, $req)
1350
    {
1351
        $result = array();
1352
        // let's accept a plain list of php parameters, beside a single xml-rpc msg object
1353
        if (is_object($req)) {
1354
            $calls = $req->getParam(0);
1355
            foreach ($calls as $call) {
1356
                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
1357
            }
1358
        } else {
1359
            $numCalls = count($req);
1360
            for ($i = 0; $i < $numCalls; $i++) {
1361
                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
1362
            }
1363
        }
1364
1365
        return new Response(new Value($result, 'array'));
1366
    }
1367
1368
    /**
1369
     * Error handler used to track errors that occur during server-side execution of PHP code.
1370
     * This allows to report back to the client whether an internal error has occurred or not
1371
     * using an xml-rpc response object, instead of letting the client deal with the html junk
1372
     * that a PHP execution error on the server generally entails.
1373
     *
1374
     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
1375
     *
1376
     * @internal
1377
     */
1378
    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
1379
    {
1380
        // obey the @ protocol
1381
        if (error_reporting() == 0) {
1382
            return;
1383
        }
1384
1385
        //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
1386
        if ($errCode != E_STRICT) {
1387
            static::error_occurred($errString);
1388
        }
1389
1390
        // Try to avoid as much as possible disruption to the previous error handling mechanism in place
1391
        if (self::$_xmlrpcs_prev_ehandler == '') {
1392
            // The previous error handler was the default: all we should do is log error to the default error log
1393
            // (if level high enough)
1394
            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
1395
                // we can't use the functionality of LoggerAware, because this is a static method
1396
                if (self::$logger === null) {
1397
                    self::$logger = Logger::instance();
1398
                }
1399
                self::$logger->error($errString);
1400
            }
1401
        } else {
1402
            // Pass control on to previous error handler, trying to avoid loops...
1403
            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...
1404
                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...
1405
                    // the following works both with static class methods and plain object methods as error handler
1406
                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
1407
                } else {
1408
                    $method = self::$_xmlrpcs_prev_ehandler;
1409
                    $method($errCode, $errString, $filename, $lineNo, $context);
1410
                }
1411
            }
1412
        }
1413
    }
1414
1415
    // *** BC layer ***
1416
1417
    /**
1418
     * @param string $charsetEncoding
1419
     * @return string
1420
     *
1421
     * @deprecated this method was moved to the Response class
1422
     */
1423
    protected function xml_header($charsetEncoding = '')
1424
    {
1425
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1426
1427
        if ($charsetEncoding != '') {
1428
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
1429
        } else {
1430
            return "<?xml version=\"1.0\"?" . ">\n";
1431
        }
1432
    }
1433
1434
    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
1435
    public function &__get($name)
1436
    {
1437
        switch ($name) {
1438
            case self::OPT_ACCEPTED_COMPRESSION :
1439
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1440
            case self::OPT_COMPRESS_RESPONSE:
1441
            case self::OPT_DEBUG:
1442
            case self::OPT_EXCEPTION_HANDLING:
1443
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1444
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1445
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1446
                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
1447
                return $this->$name;
1448
            default:
1449
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1450
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1451
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1452
                $result = null;
1453
                return $result;
1454
        }
1455
    }
1456
1457
    public function __set($name, $value)
1458
    {
1459
        switch ($name) {
1460
            case self::OPT_ACCEPTED_COMPRESSION :
1461
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1462
            case self::OPT_COMPRESS_RESPONSE:
1463
            case self::OPT_DEBUG:
1464
            case self::OPT_EXCEPTION_HANDLING:
1465
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1466
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1467
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1468
                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
1469
                $this->$name = $value;
1470
                break;
1471
            default:
1472
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1473
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1474
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1475
        }
1476
    }
1477
1478
    public function __isset($name)
1479
    {
1480
        switch ($name) {
1481
            case self::OPT_ACCEPTED_COMPRESSION :
1482
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1483
            case self::OPT_COMPRESS_RESPONSE:
1484
            case self::OPT_DEBUG:
1485
            case self::OPT_EXCEPTION_HANDLING:
1486
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1487
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1488
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1489
                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
1490
                return isset($this->$name);
1491
            default:
1492
                return false;
1493
        }
1494
    }
1495
1496
    public function __unset($name)
1497
    {
1498
        switch ($name) {
1499
            case self::OPT_ACCEPTED_COMPRESSION :
1500
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1501
            case self::OPT_COMPRESS_RESPONSE:
1502
            case self::OPT_DEBUG:
1503
            case self::OPT_EXCEPTION_HANDLING:
1504
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1505
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1506
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1507
                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
1508
                unset($this->$name);
1509
                break;
1510
            default:
1511
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1512
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1513
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1514
        }
1515
    }
1516
}
1517