Issues (320)

src/Server.php (2 issues)

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
    /** @var string */
43
    protected static $responseClass = '\\PhpXmlRpc\\Response';
44
45
    /**
46
     * @var string
47
     * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
48
     * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (the latter only for use by polyfill-xmlrpc).
49
     *
50
     * @todo create class constants for these
51
     */
52
    protected $functions_parameters_type = 'xmlrpcvals';
53
54
    /**
55
     * @var array
56
     * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map
57
     * when the functions_parameters_type member is set to 'phpvals'.
58
     * @see Encoder::encode for a list of values
59
     */
60
    protected $phpvals_encoding_options = array('auto_dates');
61
62
    /**
63
     * @var int
64
     * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
65
     * SECURITY SENSITIVE!
66
     * Valid values:
67
     * 0 =
68
     * 1 =
69
     * 2 =
70
     * 3 =
71
     */
72
    protected $debug = 1;
73
74
    /**
75
     * @var int
76
     * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
77
     * 0 = catch it and return an 'internal error' xml-rpc response (default)
78
     * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error
79
     *     corresponding to the exception, both its code and message.
80
     * 2 = allow the exception to float to the upper layers
81
     * Can be overridden per-method-handler in the dispatch map
82
     */
83
    protected $exception_handling = 0;
84
85
    /**
86
     * @var bool
87
     * When set to true, it will enable HTTP compression of the response, in case the client has declared its support
88
     * for compression in the request.
89
     * Automatically set at constructor time.
90
     */
91
    protected $compress_response = false;
92
93
    /**
94
     * @var string[]
95
     * List of http compression methods accepted by the server for requests. Automatically set at constructor time.
96
     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
97
     */
98
    protected $accepted_compression = array();
99
100
    /**
101
     * @var bool
102
     * Shall we serve calls to system.* methods?
103
     */
104
    protected $allow_system_funcs = true;
105
106
    /**
107
     * List of charset encodings natively accepted for requests.
108
     * Set at constructor time.
109
     * @deprecated UNUSED so far by this library. It is still accessible by subclasses but will be dropped in the future.
110
     */
111
    private $accepted_charset_encodings = array();
112
113
    /**
114
     * @var string
115 562
     * Charset encoding to be used for response.
116
     * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
117 562
     * Can be:
118 562
     * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled),
119
     * - null (leave unspecified in response, convert output stream to US_ASCII),
120 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).
121
     * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
122
     */
123
    protected $response_charset_encoding = '';
124
125
    protected static $options = array(
126
        self::OPT_ACCEPTED_COMPRESSION,
127
        self::OPT_ALLOW_SYSTEM_FUNCS,
128 2
        self::OPT_COMPRESS_RESPONSE,
129
        self::OPT_DEBUG,
130 2
        self::OPT_EXCEPTION_HANDLING,
131 2
        self::OPT_FUNCTIONS_PARAMETERS_TYPE,
132
        self::OPT_PHPVALS_ENCODING_OPTIONS,
133 2
        self::OPT_RESPONSE_CHARSET_ENCODING,
134
    );
135
136
    /**
137
     * @var mixed
138
     * Extra data passed at runtime to method handling functions. Used only by EPI layer
139
     * @internal
140
     */
141
    public $user_data = null;
142
143
    /**
144
     * Array defining php functions exposed as xml-rpc methods by this server.
145
     * @var array[] $dmap
146
     */
147
    protected $dmap = array();
148
149
    /**
150
     * Storage for internal debug info.
151
     */
152 562
    protected $debug_info = '';
153
154
    protected static $_xmlrpc_debuginfo = '';
155
    protected static $_xmlrpcs_occurred_errors = '';
156 562
    protected static $_xmlrpcs_prev_ehandler = '';
157 562
158 562
    /**
159
     * @param array[] $dispatchMap the dispatch map with definition of exposed services
160
     *                             Array keys are the names of the method names.
161
     *                             Each array value is an array with the following members:
162 562
     *                             - function (callable)
163
     *                             - docstring (optional)
164
     *                             - signature (array, optional)
165
     *                             - signature_docs (array, optional)
166
     *                             - parameters_type (string, optional). Valid values: 'phpvals', 'xmlrpcvals'
167
     *                             - exception_handling (int, optional)
168
     * @param boolean $serviceNow set to false in order to prevent the server from running upon construction
169
     */
170 562
    public function __construct($dispatchMap = null, $serviceNow = true)
171 561
    {
172 561
        // if ZLIB is enabled, let the server by default accept compressed requests,
173 2
        // and compress responses sent to clients that support them
174
        if (function_exists('gzinflate')) {
175
            $this->accepted_compression[] = 'gzip';
176 562
        }
177
        if (function_exists('gzuncompress')) {
178
            $this->accepted_compression[] = 'deflate';
179
        }
180
        if (function_exists('gzencode') || function_exists('gzcompress')) {
181
            $this->compress_response = true;
182
        }
183
184
        // by default the xml parser can support these 3 charset encodings
185
        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
186
187
        // dispMap is a dispatch array of methods mapped to function names and signatures.
188
        // If a method doesn't appear in the map then an unknown method error is generated.
189
        // milosch - changed to make passing dispMap optional. Instead, you can use the addToMap() function
190
        // to add functions manually (borrowed from SOAPX4)
191
        if ($dispatchMap) {
192 559
            $this->setDispatchMap($dispatchMap);
193
            if ($serviceNow) {
194 559
                $this->service();
195 559
            }
196
        }
197
    }
198
199
    /**
200
     * @param string $name see all the OPT_ constants
201
     * @param mixed $value
202
     * @return $this
203
     * @throws ValueErrorException on unsupported option
204 2
     */
205
    public function setOption($name, $value)
206 2
    {
207 2
        switch ($name) {
208
            case self::OPT_ACCEPTED_COMPRESSION :
209
            case self::OPT_ALLOW_SYSTEM_FUNCS:
210
            case self::OPT_COMPRESS_RESPONSE:
211
            case self::OPT_DEBUG:
212
            case self::OPT_EXCEPTION_HANDLING:
213
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
214
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
215
            case self::OPT_RESPONSE_CHARSET_ENCODING:
216 22
                $this->$name = $value;
217
                break;
218 22
            default:
219 22
                throw new ValueErrorException("Unsupported option '$name'");
220
        }
221
222
        return $this;
223
    }
224
225
    /**
226
     * @param string $name see all the OPT_ constants
227
     * @return mixed
228 561
     * @throws ValueErrorException on unsupported option
229
     */
230
    public function getOption($name)
231
    {
232
        switch ($name) {
233
            case self::OPT_ACCEPTED_COMPRESSION:
234
            case self::OPT_ALLOW_SYSTEM_FUNCS:
235 561
            case self::OPT_COMPRESS_RESPONSE:
236 561
            case self::OPT_DEBUG:
237 559
            case self::OPT_EXCEPTION_HANDLING:
238
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
239 561
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
240 2
            case self::OPT_RESPONSE_CHARSET_ENCODING:
241
                return $this->$name;
242
            default:
243
                throw new ValueErrorException("Unsupported option '$name'");
244
        }
245
    }
246 561
247
    /**
248
     * Returns the complete list of Server options.
249
     * @return array
250
     */
251
    public function getOptions()
252
    {
253
        $values = array();
254
        foreach(static::$options as $opt) {
255
            $values[$opt] = $this->getOption($opt);
256
        }
257
        return $values;
258
    }
259 561
260
    /**
261 561
     * @param array $options key:  see all the OPT_ constants
262 561
     * @return $this
263
     * @throws ValueErrorException on unsupported option
264 561
     */
265
    public function setOptions($options)
266
    {
267 561
        foreach($options as $name => $value) {
268
            $this->setOption($name, $value);
269
        }
270 561
271 559
        return $this;
272
    }
273
274 561
    /**
275 561
     * Set debug level of server.
276
     *
277 561
     * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments)
278
     *                    0 = no debug info,
279
     *                    1 = msgs set from user with debugmsg(),
280
     *                    2 = add complete xml-rpc request (headers and body),
281
     *                    3 = add also all processing warnings happened during method processing
282 561
     *                    (NB: this involves setting a custom error handler, and might interfere
283
     *                    with the standard processing of the php function exposed as method. In
284
     *                    particular, triggering a USER_ERROR level error will not halt script
285 561
     *                    execution anymore, but just end up logged in the xml-rpc response)
286 22
     *                    Note that info added at level 2 and 3 will be base64 encoded
287 22
     * @return $this
288
     */
289
    public function setDebug($level)
290 561
    {
291 561
        $this->debug = $level;
292 561
        return $this;
293
    }
294
295
    /**
296 561
     * Add a string to the debug info that can be later serialized by the server as part of the response message.
297 561
     * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
298
     * character set.
299 561
     *
300
     * @param string $msg
301 561
     * @return void
302
     */
303
    public static function xmlrpc_debugmsg($msg)
304
    {
305
        static::$_xmlrpc_debuginfo .= $msg . "\n";
306
    }
307 561
308 561
    /**
309
     * Add a string to the debug info that will be later serialized by the server as part of the response message
310
     * (base64 encoded) when debug level >= 2
311 561
     *
312
     * @param string $msg
313
     * @return void
314
     */
315
    public static function error_occurred($msg)
316
    {
317 561
        static::$_xmlrpcs_occurred_errors .= $msg . "\n";
318 561
    }
319 104
320
    /**
321 104
     * Return a string with the serialized representation of all debug info.
322 52
     *
323 52
     * @internal this function will become protected in the future
324 52
     *
325 52
     * @param string $charsetEncoding the target charset encoding for the serialization
326 52
     *
327 52
     * @return string an XML comment (or two)
328 52
     */
329
    public function serializeDebug($charsetEncoding = '')
330
    {
331
        // Tough encoding problem: which internal charset should we assume for debug info?
332
        // It might contain a copy of raw data received from client, ie with unknown encoding,
333
        // intermixed with php generated data and user generated data...
334
        // so we split it: system debug is base 64 encoded,
335
        // user debug info should be encoded by the end user using the INTERNAL_ENCODING
336 561
        $out = '';
337 561
        if ($this->debug_info != '') {
338
            $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
339
        }
340
        if (static::$_xmlrpc_debuginfo != '') {
341
            $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
342
            // NB: a better solution MIGHT be to use CDATA, but we need to insert it
343 561
            // into return payload AFTER the beginning tag
344
            //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
345
        }
346 561
347
        return $out;
348
    }
349
350
    /**
351
     * Execute the xml-rpc request, printing the response.
352
     *
353
     * @param string $data the request body. If null, the http POST request will be examined
354
     * @param bool $returnPayload When true, return the response but do not echo it or any http header
355
     *
356
     * @return Response|string the response object (usually not used by caller...) or its xml serialization
357
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
358
     */
359
    public function service($data = null, $returnPayload = false)
360
    {
361
        if ($data === null) {
362
            $data = file_get_contents('php://input');
363
        }
364
        $rawData = $data;
365
366
        // reset internal debug info
367
        $this->debug_info = '';
368
369
        // Save what we received, before parsing it
370
        if ($this->debug > 1) {
371
            $this->debugMsg("+++GOT+++\n" . $data . "\n+++END+++");
372
        }
373
374
        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
375
        if (!$resp) {
376
            // this actually executes the request
377
            $resp = $this->parseRequest($data, $reqCharset);
378
379
            // save full body of request into response, for debugging purposes.
380
            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
381
            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
382
            ///       better: just avoid setting this, and set debug info of the received http request in the request
383
            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
384
            ///       Last but not least: the raw data might be of use to handler functions - but in decompressed form...
385
            $resp->raw_data = $rawData;
386 536
        }
387
388
        if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
389 536
            $this->debugMsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
390 536
                static::$_xmlrpcs_occurred_errors . "+++END+++");
391
        }
392
393
        $payload = $this->generatePayload($resp, $respCharset);
394 536
395 536
        if ($returnPayload) {
396 536
            return $payload;
397 536
        }
398 515
399 515
        $this->printPayload($payload, $resp->getContentType(), $respEncoding);
400 515
401 452
        // return response, in case subclasses want it
402
        return $resp;
403 148
    }
404
405
    /**
406
     * Add a method to the dispatch map.
407
     *
408
     * @param string $methodName the name with which the method will be made available
409
     * @param callable $function the php function that will get invoked
410 515
     * @param array[] $sig the array of valid method signatures.
411 22
     *                     Each element is one signature: an array of strings with at least one element
412 22
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
413 22
     * @param string $doc method documentation
414 22
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
415 22
     *                        descriptions instead of types (one string for return type, one per param)
416
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
417
     * @param int $exceptionHandling @see $this->exception_handling
418 536
     * @return void
419 536
     *
420
     * @todo raise a warning if the user tries to register a 'system.' method - but allow users to do that
421
     */
422
    public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
423 22
        $exceptionHandling = false)
424
    {
425
       $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);
426 22
    }
427
428
    /**
429
     * Add a method to the dispatch map.
430
     *
431
     * @param string $methodName the name with which the method will be made available
432
     * @param callable $function the php function that will get invoked
433
     * @param array[] $sig the array of valid method signatures.
434
     *                     Each element is one signature: an array of strings with at least one element
435 561
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
436
     * @param string $doc method documentation
437
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
438
     *                        descriptions instead of types (one string for return type, one per param)
439 561
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
440
     * @param int $exceptionHandling @see $this->exception_handling
441
     * @return void
442
     *
443 561
     * @todo raise a warning if the user tries to register a 'system.' method
444 559
     * @deprecated use addToMap instead
445 559
     */
446 559
    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
447 559
        $exceptionHandling = false)
448
    {
449
        $this->logDeprecationUnlessCalledBy('addToMap');
450
451
        $this->dmap[$methodName] = array(
452 561
            'function' => $function,
453 104
            'docstring' => $doc,
454
        );
455 457
        if ($sig) {
456
            $this->dmap[$methodName]['signature'] = $sig;
457
        }
458 561
        if ($sigDoc) {
459
            $this->dmap[$methodName]['signature_docs'] = $sigDoc;
460
        }
461 561
        if ($parametersType) {
462 104
            $this->dmap[$methodName]['parameters_type'] = $parametersType;
463
        }
464 104
        if ($exceptionHandling !== false) {
465 104
            $this->dmap[$methodName]['exception_handling'] = $exceptionHandling;
466 52
        }
467 52
    }
468 52
469
    /**
470 52
     * Verify type and number of parameters received against a list of known signatures.
471 52
     *
472 52
     * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
473 52
     * @param array $sigs array of known signatures to match against
474
     * @return array int, string
475
     */
476
    protected function verifySignature($in, $sigs)
477
    {
478
        // check each possible signature in turn
479
        if (is_object($in)) {
480
            $numParams = $in->getNumParams();
481
        } else {
482
            $numParams = count($in);
483
        }
484
        foreach ($sigs as $curSig) {
485
            if (count($curSig) == $numParams + 1) {
486
                $itsOK = 1;
487
                for ($n = 0; $n < $numParams; $n++) {
488
                    if (is_object($in)) {
489
                        $p = $in->getParam($n);
490
                        if ($p->kindOf() == 'scalar') {
491
                            $pt = $p->scalarTyp();
492
                        } else {
493
                            $pt = $p->kindOf();
494 561
                        }
495
                    } else {
496
                        $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
497
                    }
498
499
                    // param index is $n+1, as first member of sig is return type
500
                    if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
501
                        $itsOK = 0;
502
                        $pno = $n + 1;
503
                        $wanted = $curSig[$n + 1];
504
                        $got = $pt;
505
                        break;
506
                    }
507
                }
508
                if ($itsOK) {
509
                    return array(1, '');
510
                }
511
            }
512
        }
513
        if (isset($wanted)) {
514
            return array(0, "Wanted {$wanted}, got {$got} at param {$pno}");
515
        } else {
516 561
            return array(0, "No method signature matches number of parameters");
517
        }
518
    }
519 561
520 104
    /**
521
     * Parse http headers received along with xml-rpc request. If needed, inflate request.
522 457
     *
523
     * @return Response|null null on success or an error Response
524
     */
525
    protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
526
    {
527 561
        // check if $_SERVER is populated: it might have been disabled via ini file
528
        // (this is true even when in CLI mode)
529
        if (count($_SERVER) == 0) {
530 561
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
531
        }
532
533
        if ($this->debug > 1) {
534
            if (function_exists('getallheaders')) {
535
                $this->debugMsg(''); // empty line
536
                foreach (getallheaders() as $name => $val) {
537
                    $this->debugMsg("HEADER: $name: $val");
538
                }
539
            }
540
        }
541
542
        if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
543
            $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
544
        } else {
545
            $contentEncoding = '';
546
        }
547 562
548
        $rawData = $data;
549
550
        // check if request body has been compressed and decompress it
551 562
        if ($contentEncoding != '' && strlen($data)) {
552
            if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
553
                // if decoding works, use it. else assume data wasn't gzencoded
554
                /// @todo test separately for gzinflate and gzuncompress
555
                if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
556
                    if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
557
                        $data = $degzdata;
558
                        if ($this->debug > 1) {
559
                            $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
560 561
                        }
561 4
                    } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
562 2
                        $data = $degzdata;
563
                        if ($this->debug > 1) {
564 2
                            $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
565 2
                        }
566
                    } else {
567
                        $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
568
                            PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
569
                        );
570
571
                        return $r;
572
                    }
573
                } else {
574
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
575
                        PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
576
                    );
577 562
578
                    return $r;
579
                }
580
            }
581 562
        }
582
583
        // check if client specified accepted charsets, and if we know how to fulfill the request
584 562
        if ($this->response_charset_encoding == 'auto') {
585 562
            $respEncoding = '';
586 562
            if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
587
                // here we check if we can match the client-requested encoding with the encodings we know we can generate.
588 2
                // we parse q=0.x preferences instead of preferring the first charset specified
589 2
                $http = new Http();
590 2
                $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
591 2
                $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
592 560
                foreach ($clientAcceptedCharsets as $accepted) {
593 1
                    foreach ($knownCharsets as $charset) {
594 1
                        if (strtoupper($accepted) == strtoupper($charset)) {
595 1
                            $respEncoding = $charset;
596
                            break 2;
597
                        }
598
                    }
599
                }
600
            }
601 559
        } else {
602 559
            $respEncoding = $this->response_charset_encoding;
603
        }
604
605
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
606
            $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
607
        } else {
608
            $respCompression = '';
609
        }
610
611
        // 'guestimate' request encoding
612 559
        /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
613
        $parser = $this->getParser();
614 559
        $reqEncoding = $parser->guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
615 538
            $data);
616
617
        return null;
618 559
    }
619 559
620
    /**
621 559
     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
622
     * server.
623
     * @internal this function will become protected in the future
624
     *
625 562
     * @param string $data the xml request
626
     * @param string $reqEncoding (optional) the charset encoding of the xml request
627
     * @return Response
628
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
629
     *
630
     * @todo either rename this function or, probably better, move the 'execute' part out of it...
631
     */
632
    public function parseRequest($data, $reqEncoding = '')
633
    {
634
        // decompose incoming XML into request structure
635
636
        /// @todo move this block of code into the XMLParser
637
        if ($reqEncoding != '') {
638
            // Since parsing will fail if
639 559
            // - charset is not specified in the xml declaration,
640
            // - the encoding is not UTF8 and
641 559
            // - there are non-ascii chars in the text,
642 559
            // we try to work round that...
643
            // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
644 559
            //if (!is_valid_charset($reqEncoding, array('UTF-8')))
645 559
            if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
646
                if (function_exists('mb_convert_encoding')) {
647
                    $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
648
                } else {
649 559
                    if ($reqEncoding == 'ISO-8859-1') {
650 559
                        $data = utf8_encode($data);
651
                    } else {
652 559
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
653
                    }
654 85
                }
655 85
            }
656 85
        }
657
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
658
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
659
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
660 559
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
661 536
        } else {
662 536
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
663 536
        }
664
        // register a callback with the xml parser for when it finds the method name
665
        $options['methodname_callback'] = array($this, 'methodNameCallback');
666
667 536
        $xmlRpcParser = $this->getParser();
668
        try {
669 22
            // NB: during parsing, the actual type of php values built will be automatically switched from
670 22
            // $this->functions_parameters_type to the one defined in the method signature, if defined there. This
671 22
            // happens via the parser making a call to $this->methodNameCallback as soon as it finds the desired method
672 22
            $_xh = $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
673
            // BC
674
            if (!is_array($_xh)) {
675
                $_xh = $xmlRpcParser->_xh;
676
            }
677 559
        } catch (NoSuchMethodException $e) {
678
            return new static::$responseClass(0, $e->getCode(), $e->getMessage());
679 559
        }
680 127
681
        if ($_xh['isf'] == 3) {
682
            // (BC) we return XML error as a faultCode
683 559
            preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
684 150
            return new static::$responseClass(
685 24
                0,
686
                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
687 127
                $_xh['isf_reason']);
688
        } elseif ($_xh['isf']) {
689 431
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
690 129
            ///       parsing error
691
            return new static::$responseClass(
692 324
                0,
693
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
694
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
695
        } else {
696 559
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
697
            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
698
            // that would mean a useless encode+decode pass
699
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
700
                (isset($this->dmap[$_xh['method']]['parameters_type']) &&
701
                    ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
702
                )
703
            ) {
704
                if ($this->debug > 1) {
705
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
706
                }
707 559
708 559
                return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
709
            } else {
710
                // build a Request object with data parsed from xml and add parameters in
711
                $req = new Request($_xh['method']);
712
                /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)...
713 559
                for ($i = 0; $i < count($_xh['params']); $i++) {
714 559
                    $req->addParam($_xh['params'][$i]);
715 127
                }
716
717 454
                if ($this->debug > 1) {
718
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
719 557
                }
720
721
                return $this->execute($req);
722
            }
723
        }
724
    }
725
726
    /**
727
     * Execute a method invoked by the client, checking parameters used.
728
     *
729
     * @param Request|string $req either a Request obj or a method name
730
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
731
     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
732
     * @return Response
733
     *
734
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
735
     */
736
    protected function execute($req, $params = null, $paramTypes = null)
737
    {
738
        static::$_xmlrpcs_occurred_errors = '';
739
        static::$_xmlrpc_debuginfo = '';
740
741
        if (is_object($req)) {
742
            $methodName = $req->method();
743
        } else {
744
            $methodName = $req;
745
        }
746
747
        $sysCall = $this->isSyscall($methodName);
748
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
749
750
        if (!isset($dmap[$methodName]['function'])) {
751
            // No such method
752
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
753
        }
754
755
        // Check signature
756
        if (isset($dmap[$methodName]['signature'])) {
757
            $sig = $dmap[$methodName]['signature'];
758
            if (is_object($req)) {
759
                list($ok, $errStr) = $this->verifySignature($req, $sig);
760
            } else {
761
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
762 45
            }
763
            if (!$ok) {
764
                // Didn't match.
765 45
                return new static::$responseClass(
766 45
                    0,
767
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
768
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
769
                );
770
            }
771
        }
772
773
        $func = $dmap[$methodName]['function'];
774
775 45
        // let the 'class::function' syntax be accepted in dispatch maps
776 2
        if (is_string($func) && strpos($func, '::')) {
777 2
            $func = explode('::', $func);
778
        }
779 45
780
        // build string representation of function 'name'
781
        if (is_array($func)) {
782 559
            if (is_object($func[0])) {
783
                $funcName = get_class($func[0]) . '->' . $func[1];
784
            } else {
785 559
                $funcName = implode('::', $func);
786 64
            }
787
        } else if ($func instanceof \Closure) {
788 496
            $funcName = 'Closure';
789
        } else {
790
            $funcName = $func;
791
        }
792 559
793
        // verify that function to be invoked is in fact callable
794
        if (!is_callable($func)) {
795
            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
796
            return new static::$responseClass(
797
                0,
798
                PhpXmlRpc::$xmlrpcerr['server_error'],
799
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
800 559
            );
801
        }
802 559
803 559
        if (isset($dmap[$methodName]['exception_handling'])) {
804
            $exception_handling = (int)$dmap[$methodName]['exception_handling'];
805
        } else {
806
            $exception_handling = $this->exception_handling;
807
        }
808
809 561
        // We always catch all errors generated during processing of user function, and log them as part of response;
810
        // if debug level is 3 or above, we also serialize them in the response as comments
811 561
        self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
812 52
813
        /// @todo what about using output-buffering as well, in case user code echoes anything to screen?
814 509
815
        try {
816
            // Allow mixed-convention servers
817
            if (is_object($req)) {
818
                // call an 'xml-rpc aware' function
819
                if ($sysCall) {
820
                    $r = call_user_func($func, $this, $req);
821
                } else {
822 559
                    $r = call_user_func($func, $req);
823
                }
824 559
                if (!is_a($r, 'PhpXmlRpc\Response')) {
825
                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
826
                    if (is_a($r, 'PhpXmlRpc\Value')) {
827
                        $r = new static::$responseClass($r);
828
                    } else {
829
                        $r = new static::$responseClass(
830
                            0,
831
                            PhpXmlRpc::$xmlrpcerr['server_error'],
832
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
833
                        );
834
                    }
835
                }
836
            } else {
837
                // call a 'plain php' function
838 127
                if ($sysCall) {
839
                    array_unshift($params, $this);
840 127
                    $r = call_user_func_array($func, $params);
841
                } else {
842
                    // 3rd API convention for method-handling functions: EPI-style
843
                    if ($this->functions_parameters_type == 'epivals') {
844
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
845
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
846 127
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
847
                            $r = new static::$responseClass(0, (int)$r['faultCode'], (string)$r['faultString']);
848
                        } else {
849 127
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
850 127
                            // return type correctly
851
                            $encoder = new Encoder();
852
                            $r = new static::$responseClass($encoder->encode($r, array('extension_api')));
853
                        }
854 127
                    } else {
855 127
                        $r = call_user_func_array($func, $params);
856 127
                    }
857
                }
858
                // the return type can be either a Response object or a plain php value...
859
                if (!is_a($r, '\PhpXmlRpc\Response')) {
860 127
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
861 127
                    // a: let the user decide
862 127
                    $encoder = new Encoder();
863
                    $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options));
864
                }
865
            }
866 127
        /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
867 127
        } catch (\Exception $e) {
868 127
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
869
            // proper error-response
870
            switch ($exception_handling) {
871
                case 2:
872 127
                    if (self::$_xmlrpcs_prev_ehandler) {
873 127
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
874 127
                        self::$_xmlrpcs_prev_ehandler = null;
875
                    } else {
876
                        restore_error_handler();
877
                    }
878
                    throw $e;
879
                case 1:
880
                    $errCode = $e->getCode();
881
                    if ($errCode == 0) {
882
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
883
                    }
884
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
885
                    break;
886
                default:
887
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
888
            }
889
        } catch (\Error $e) {
890
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
891
            // proper error-response
892
            switch ($exception_handling) {
893
                case 2:
894
                    if (self::$_xmlrpcs_prev_ehandler) {
895
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
896
                        self::$_xmlrpcs_prev_ehandler = null;
897
                    } else {
898
                        restore_error_handler();
899
                    }
900
                    throw $e;
901
                case 1:
902
                    $errCode = $e->getCode();
903
                    if ($errCode == 0) {
904
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
905
                    }
906
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
907
                    break;
908
                default:
909
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
910
            }
911
        }
912
913
        // note: restore the error handler we found before calling the user func, even if it has been changed
914
        // inside the func itself
915
        if (self::$_xmlrpcs_prev_ehandler) {
916
            set_error_handler(self::$_xmlrpcs_prev_ehandler);
917
            self::$_xmlrpcs_prev_ehandler = null;
918
        } else {
919
            restore_error_handler();
920
        }
921
922
        return $r;
923
    }
924
925
    /**
926
     * @param Response $resp
927
     * @param string $respCharset
928
     * @return string
929
     */
930
    protected function generatePayload($resp, $respCharset)
931
    {
932
        $header = $resp->xml_header($respCharset);
933 22
        if ($this->debug > 0) {
934
            $header .= $this->serializeDebug($respCharset);
935 22
        }
936 22
937 22
        // Do not create response serialization if it has already happened. Helps to build json magic
938
        /// @todo what if the payload was created targeting a different charset than $respCharset?
939 22
        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
940 22
        $payload = $resp->getPayload();
941
        if (empty($payload)) {
942
            $payload = $resp->serialize($respCharset);
943 22
        }
944
945
        return $header . $payload;
946
    }
947
948
    /**
949
     * @param string $payload
950
     * @param string $respContentType
951 106
     * @param string $respEncoding
952
     * @return void
953
     */
954 106
    protected function printPayload($payload, $respContentType, $respEncoding)
955 106
    {
956 106
        // if we get a warning/error that has output some text before here, then we cannot
957
        // add a new header. We cannot say we are sending xml, either...
958
        if (!headers_sent()) {
959
            header('Content-Type: ' . $respContentType);
960 106
            // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
961 85
            header("Vary: Accept-Charset");
962
963 22
            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
964
            // and php ini settings do not force it already
965 106
            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
966 106
            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
967 106
                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
968 106
                    $payload = gzencode($payload);
969 106
                    header("Content-Encoding: gzip");
970 106
                    header("Vary: Accept-Encoding");
971 106
                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
972
                    $payload = gzcompress($payload);
973 106
                    header("Content-Encoding: deflate");
974
                    header("Vary: Accept-Encoding");
975 106
                }
976
            }
977
978
            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
979
            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
980
            // responses up to 8000 bytes
981
            if ($phpNoSelfCompress) {
982 1
                header('Content-Length: ' . (int)strlen($payload));
983
            }
984
        } else {
985 106
            /// @todo allow the user to easily subclass this in a way which allows the resp. headers to be already sent
986
            ///       by now without flagging it as an error. Possibly check for presence of Content-Type header
987
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
988
        }
989
990
        print $payload;
991
    }
992
993 85
    /**
994
     * Registered as callback for when the XMLParser has found the name of the method to execute.
995
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
996 85
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
997 85
     *
998 85
     * @internal
999
     * @param $methodName
1000
     * @param XMLParser $xmlParser
1001
     * @param null|resource $parser
1002 85
     * @return void
1003 85
     * @throws NoSuchMethodException
1004
     *
1005 1
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
1006
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
1007 85
     *       about the matched method handler, in order to avoid doing the work twice...
1008 85
     */
1009 85
    public function methodNameCallback($methodName, $xmlParser, $parser = null)
1010
    {
1011
        $sysCall = $this->isSyscall($methodName);
1012
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
1013
1014
        if (!isset($dmap[$methodName]['function'])) {
1015
            // No such method
1016
            throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
1017 85
        }
1018
1019
        // alter on-the-fly the config of the xml parser if needed
1020 64
        if (isset($dmap[$methodName]['parameters_type']) &&
1021
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
1022 64
            /// @todo this should be done by a method of the XMLParser
1023 64
            switch ($dmap[$methodName]['parameters_type']) {
1024 64
                case XMLParser::RETURN_PHP:
1025
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee_fast'));
1026 64
                    break;
1027 64
                case XMLParser::RETURN_EPIVALS:
1028
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee_epi'));
1029 64
                    break;
1030 64
                /// @todo log a warning on unsupported return type
1031 64
                case XMLParser::RETURN_XMLRPCVALS:
1032
                default:
1033 64
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee'));
1034
            }
1035
        }
1036
    }
1037
1038
    /**
1039
     * Add a string to the 'internal debug message' (separate from 'user debug message').
1040
     *
1041 64
     * @param string $string
1042
     * @return void
1043 64
     */
1044
    protected function debugMsg($string)
1045
    {
1046 64
        $this->debug_info .= $string . "\n";
1047 64
    }
1048
1049
    /**
1050 64
     * @param string $methName
1051
     * @return bool
1052
     */
1053 64
    protected function isSyscall($methName)
1054 64
    {
1055
        return (strpos($methName, "system.") === 0);
1056
    }
1057 64
1058 64
    /**
1059
     * @param array $dmap
1060
     * @return $this
1061 64
     */
1062
    public function setDispatchMap($dmap)
1063
    {
1064
        $this->dmap = $dmap;
1065 64
        return $this;
1066 64
    }
1067 64
1068
    /**
1069
     * @return array[]
1070
     */
1071
    public function getDispatchMap()
1072
    {
1073
        return $this->dmap;
1074
    }
1075 64
1076
    /**
1077 64
     * @return array[]
1078 64
     */
1079
    public function getSystemDispatchMap()
1080
    {
1081 64
        if (!$this->allow_system_funcs) {
1082
            return array();
1083
        }
1084
1085
        return array(
1086
            'system.listMethods' => array(
1087
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
1088
                // listMethods: signature was either a string, or nothing.
1089
                // The useless string variant has been removed
1090
                'signature' => array(array(Value::$xmlrpcArray)),
1091
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
1092
                'signature_docs' => array(array('list of method names')),
1093
            ),
1094
            'system.methodHelp' => array(
1095
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
1096
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
1097
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
1098
                'signature_docs' => array(array('method description', 'name of the method to be described')),
1099
            ),
1100
            'system.methodSignature' => array(
1101
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
1102
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
1103
                '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)',
1104
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
1105
            ),
1106
            'system.multicall' => array(
1107
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
1108
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
1109
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
1110
                '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"')),
1111
            ),
1112
            'system.getCapabilities' => array(
1113
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
1114
                'signature' => array(array(Value::$xmlrpcStruct)),
1115
                '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',
1116
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
1117
            ),
1118
        );
1119
    }
1120
1121
    /**
1122
     * @return array[]
1123
     */
1124
    public function getCapabilities()
1125
    {
1126
        $outAr = array(
1127
            // xml-rpc spec: always supported
1128
            'xmlrpc' => array(
1129
                // NB: the spec sits now at https://xmlrpc.com/spec.md
1130
                'specUrl' => 'http://www.xmlrpc.com/spec',
1131
                'specVersion' => 1
1132
            ),
1133
            // if we support system.xxx functions, we always support multicall, too...
1134
            'system.multicall' => array(
1135
                // Note that, as of 2006/09/17, the following URL does not respond anymore
1136
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
1137 85
                'specVersion' => 1
1138
            ),
1139 85
            // introspection: version 2! we support 'mixed', too.
1140
            // note: the php xml-rpc extension says this instead:
1141 85
            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
1142 85
            'introspection' => array(
1143 85
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
1144 64
                'specVersion' => 2,
1145
            ),
1146
        );
1147
1148
        // NIL extension
1149
        if (PhpXmlRpc::$xmlrpc_null_extension) {
1150
            $outAr['nil'] = array(
1151
                // Note that, as of 2023/01, the following URL does not respond anymore
1152
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
1153 85
                'specVersion' => 1
1154
            );
1155
        }
1156
1157
        // support for "standard" error codes
1158
        if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
1159
            $outAr['faults_interop'] = array(
1160
                'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
1161
                'specVersion' => 20010516
1162
            );
1163
        }
1164 43
1165
        return $outAr;
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_getCapabilities($server, $req = null)
0 ignored issues
show
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

1175
    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...
1176
    {
1177 22
        $encoder = new Encoder();
1178
        return new static::$responseClass($encoder->encode($server->getCapabilities()));
1179
    }
1180 22
1181 22
    /**
1182 22
     * @internal handler of a system. method
1183
     *
1184 22
     * @param Server $server
1185
     * @param Request $req if called in plain php values mode, second param is missing
1186
     * @return Response
1187
     */
1188
    public static function _xmlrpcs_listMethods($server, $req = null)
0 ignored issues
show
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

1188
    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...
1189
    {
1190
        $outAr = array();
1191
        foreach ($server->dmap as $key => $val) {
1192
            $outAr[] = new Value($key, 'string');
1193
        }
1194
        foreach ($server->getSystemDispatchMap() as $key => $val) {
1195
            $outAr[] = new Value($key, 'string');
1196
        }
1197
1198 22
        return new static::$responseClass(new Value($outAr, 'array'));
1199
    }
1200
1201
    /**
1202
     * @internal handler of a system. method
1203
     *
1204
     * @param Server $server
1205
     * @param Request $req
1206
     * @return Response
1207
     */
1208
    public static function _xmlrpcs_methodSignature($server, $req)
1209
    {
1210
        // let's accept as parameter either an xml-rpc value or string
1211
        if (is_object($req)) {
1212
            $methName = $req->getParam(0);
1213
            $methName = $methName->scalarVal();
1214
        } else {
1215
            $methName = $req;
1216
        }
1217
        if ($server->isSyscall($methName)) {
1218
            $dmap = $server->getSystemDispatchMap();
1219
        } else {
1220
            $dmap = $server->dmap;
1221
        }
1222
        if (isset($dmap[$methName])) {
1223
            if (isset($dmap[$methName]['signature'])) {
1224
                $sigs = array();
1225
                foreach ($dmap[$methName]['signature'] as $inSig) {
1226
                    $curSig = array();
1227
                    foreach ($inSig as $sig) {
1228
                        $curSig[] = new Value($sig, 'string');
1229
                    }
1230
                    $sigs[] = new Value($curSig, 'array');
1231
                }
1232
                $r = new static::$responseClass(new Value($sigs, 'array'));
1233
            } else {
1234
                // NB: according to the official docs, we should be returning a
1235
                // "none-array" here, which means not-an-array
1236
                $r = new static::$responseClass(new Value('undef', 'string'));
1237
            }
1238
        } else {
1239
            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1240
        }
1241
1242
        return $r;
1243
    }
1244
1245
    /**
1246
     * @internal handler of a system. method
1247
     *
1248
     * @param Server $server
1249
     * @param Request $req
1250
     * @return Response
1251
     */
1252
    public static function _xmlrpcs_methodHelp($server, $req)
1253
    {
1254
        // let's accept as parameter either an xml-rpc value or string
1255
        if (is_object($req)) {
1256
            $methName = $req->getParam(0);
1257
            $methName = $methName->scalarVal();
1258
        } else {
1259
            $methName = $req;
1260
        }
1261
        if ($server->isSyscall($methName)) {
1262
            $dmap = $server->getSystemDispatchMap();
1263
        } else {
1264
            $dmap = $server->dmap;
1265
        }
1266
        if (isset($dmap[$methName])) {
1267
            if (isset($dmap[$methName]['docstring'])) {
1268
                $r = new static::$responseClass(new Value($dmap[$methName]['docstring'], 'string'));
1269
            } else {
1270
                $r = new static::$responseClass(new Value('', 'string'));
1271
            }
1272
        } else {
1273
            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1274
        }
1275
1276
        return $r;
1277
    }
1278
1279
    /**
1280
     * @internal this function will become protected in the future
1281
     *
1282
     * @param $err
1283
     * @return Value
1284
     */
1285
    public static function _xmlrpcs_multicall_error($err)
1286
    {
1287
        if (is_string($err)) {
1288
            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
1289
            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
1290
        } else {
1291
            $code = $err->faultCode();
1292
            $str = $err->faultString();
1293
        }
1294
        $struct = array();
1295
        $struct['faultCode'] = new Value($code, 'int');
1296
        $struct['faultString'] = new Value($str, 'string');
1297
1298
        return new Value($struct, 'struct');
1299
    }
1300
1301
    /**
1302
     * @internal this function will become protected in the future
1303
     *
1304
     * @param Server $server
1305
     * @param Value $call
1306
     * @return Value
1307
     */
1308
    public static function _xmlrpcs_multicall_do_call($server, $call)
1309
    {
1310
        if ($call->kindOf() != 'struct') {
1311
            return static::_xmlrpcs_multicall_error('notstruct');
1312
        }
1313
        $methName = @$call['methodName'];
1314
        if (!$methName) {
1315
            return static::_xmlrpcs_multicall_error('nomethod');
1316
        }
1317
        if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
1318
            return static::_xmlrpcs_multicall_error('notstring');
1319
        }
1320
        if ($methName->scalarVal() == 'system.multicall') {
1321
            return static::_xmlrpcs_multicall_error('recursion');
1322
        }
1323
1324
        $params = @$call['params'];
1325
        if (!$params) {
1326
            return static::_xmlrpcs_multicall_error('noparams');
1327
        }
1328
        if ($params->kindOf() != 'array') {
1329
            return static::_xmlrpcs_multicall_error('notarray');
1330
        }
1331
1332
        $req = new Request($methName->scalarVal());
1333
        foreach ($params as $i => $param) {
1334
            if (!$req->addParam($param)) {
1335
                $i++; // for error message, we count params from 1
1336
                return static::_xmlrpcs_multicall_error(new static::$responseClass(0,
1337
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1338
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1339
            }
1340
        }
1341
1342
        $result = $server->execute($req);
1343
1344
        if ($result->faultCode() != 0) {
1345
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1346
        }
1347
1348
        return new Value(array($result->value()), 'array');
1349
    }
1350
1351
    /**
1352
     * @internal this function will become protected in the future
1353
     *
1354
     * @param Server $server
1355
     * @param Value $call
1356
     * @return Value
1357
     */
1358
    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1359
    {
1360
        if (!is_array($call)) {
1361
            return static::_xmlrpcs_multicall_error('notstruct');
1362
        }
1363
        if (!array_key_exists('methodName', $call)) {
1364
            return static::_xmlrpcs_multicall_error('nomethod');
1365
        }
1366
        if (!is_string($call['methodName'])) {
1367
            return static::_xmlrpcs_multicall_error('notstring');
1368
        }
1369
        if ($call['methodName'] == 'system.multicall') {
1370
            return static::_xmlrpcs_multicall_error('recursion');
1371
        }
1372
        if (!array_key_exists('params', $call)) {
1373
            return static::_xmlrpcs_multicall_error('noparams');
1374
        }
1375
        if (!is_array($call['params'])) {
1376
            return static::_xmlrpcs_multicall_error('notarray');
1377
        }
1378
1379
        // this is a simplistic hack, since we might have received
1380
        // base64 or datetime values, but they will be listed as strings here...
1381
        $pt = array();
1382
        $wrapper = new Wrapper();
1383
        foreach ($call['params'] as $val) {
1384
            // support EPI-encoded base64 and datetime values
1385
            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
1386
                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
1387
            } else {
1388
                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
1389
            }
1390
        }
1391
1392
        $result = $server->execute($call['methodName'], $call['params'], $pt);
1393
1394
        if ($result->faultCode() != 0) {
1395
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1396
        }
1397
1398
        return new Value(array($result->value()), 'array');
1399
    }
1400
1401
    /**
1402
     * @internal handler of a system. method
1403
     *
1404
     * @param Server $server
1405
     * @param Request|array $req
1406
     * @return Response
1407
     */
1408
    public static function _xmlrpcs_multicall($server, $req)
1409
    {
1410
        $result = array();
1411
        // let's accept a plain list of php parameters, beside a single xml-rpc msg object
1412
        if (is_object($req)) {
1413
            $calls = $req->getParam(0);
1414
            foreach ($calls as $call) {
1415
                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
1416
            }
1417
        } else {
1418
            $numCalls = count($req);
1419
            for ($i = 0; $i < $numCalls; $i++) {
1420
                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
1421
            }
1422
        }
1423
1424
        return new static::$responseClass(new Value($result, 'array'));
1425
    }
1426
1427
    /**
1428
     * Error handler used to track errors that occur during server-side execution of PHP code.
1429
     * This allows to report back to the client whether an internal error has occurred or not
1430
     * using an xml-rpc response object, instead of letting the client deal with the html junk
1431
     * that a PHP execution error on the server generally entails.
1432
     *
1433
     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
1434
     *
1435
     * @internal
1436
     */
1437
    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
1438
    {
1439
        // obey the @ protocol
1440
        if (error_reporting() == 0) {
1441
            return;
1442
        }
1443
1444
        // From PHP 8.4 the E_STRICT constant has been deprecated and will emit deprecation notices.
1445
        // PHP core and core extensions since PHP 8.0 and later do not emit E_STRICT notices at all.
1446
        // On PHP 7 series before PHP 7.4, some functions conditionally emit E_STRICT notices.
1447
        if (PHP_VERSION_ID >= 70400) {
1448
            static::error_occurred($errString);
1449
        } elseif ($errCode != E_STRICT) {
1450
            static::error_occurred($errString);
1451
        }
1452
1453
        // Try to avoid as much as possible disruption to the previous error handling mechanism in place
1454
        if (self::$_xmlrpcs_prev_ehandler == '') {
1455
            // The previous error handler was the default: all we should do is log error to the default error log
1456
            // (if level high enough)
1457
            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
1458
                // we can't use the functionality of LoggerAware, because this is a static method
1459
                if (self::$logger === null) {
1460
                    self::$logger = Logger::instance();
1461
                }
1462
                self::$logger->error($errString);
1463
            }
1464
        } else {
1465
            // Pass control on to previous error handler, trying to avoid loops...
1466
            if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) {
1467
                if (is_array(self::$_xmlrpcs_prev_ehandler)) {
1468
                    // the following works both with static class methods and plain object methods as error handler
1469
                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
1470
                } else {
1471
                    $method = self::$_xmlrpcs_prev_ehandler;
1472
                    $method($errCode, $errString, $filename, $lineNo, $context);
1473
                }
1474
            }
1475
        }
1476
    }
1477
1478
    // *** BC layer ***
1479
1480
    /**
1481
     * @param string $charsetEncoding
1482
     * @return string
1483
     *
1484
     * @deprecated this method was moved to the Response class
1485
     */
1486
    protected function xml_header($charsetEncoding = '')
1487
    {
1488
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1489
1490
        if ($charsetEncoding != '') {
1491
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
1492
        } else {
1493
            return "<?xml version=\"1.0\"?" . ">\n";
1494
        }
1495
    }
1496
1497
    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
1498
    public function &__get($name)
1499
    {
1500
        switch ($name) {
1501
            case self::OPT_ACCEPTED_COMPRESSION :
1502
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1503
            case self::OPT_COMPRESS_RESPONSE:
1504
            case self::OPT_DEBUG:
1505
            case self::OPT_EXCEPTION_HANDLING:
1506
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1507
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1508
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1509
                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
1510
                return $this->$name;
1511
            case 'accepted_charset_encodings':
1512
                // manually implement the 'protected property' behaviour
1513
                $canAccess = false;
1514
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1515
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1516
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1517
                        $canAccess = true;
1518
                    }
1519
                }
1520
                if ($canAccess) {
1521
                    $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
1522
                    return $this->accepted_compression;
1523
                } else {
1524
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1525
                }
1526
                break;
1527
            default:
1528
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1529
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1530
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1531
                $result = null;
1532
                return $result;
1533
        }
1534
    }
1535
1536
    public function __set($name, $value)
1537
    {
1538
        switch ($name) {
1539
            case self::OPT_ACCEPTED_COMPRESSION :
1540
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1541
            case self::OPT_COMPRESS_RESPONSE:
1542
            case self::OPT_DEBUG:
1543
            case self::OPT_EXCEPTION_HANDLING:
1544
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1545
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1546
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1547
                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
1548
                $this->$name = $value;
1549
                break;
1550
            case 'accepted_charset_encodings':
1551
                // manually implement the 'protected property' behaviour
1552
                $canAccess = false;
1553
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1554
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1555
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1556
                        $canAccess = true;
1557
                    }
1558
                }
1559
                if ($canAccess) {
1560
                    $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
1561
                    $this->accepted_compression = $value;
1562
                } else {
1563
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1564
                }
1565
                break;
1566
            default:
1567
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1568
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1569
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1570
        }
1571
    }
1572
1573
    public function __isset($name)
1574
    {
1575
        switch ($name) {
1576
            case self::OPT_ACCEPTED_COMPRESSION :
1577
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1578
            case self::OPT_COMPRESS_RESPONSE:
1579
            case self::OPT_DEBUG:
1580
            case self::OPT_EXCEPTION_HANDLING:
1581
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1582
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1583
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1584
                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
1585
                return isset($this->$name);
1586
            case 'accepted_charset_encodings':
1587
                // manually implement the 'protected property' behaviour
1588
                $canAccess = false;
1589
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1590
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1591
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1592
                        $canAccess = true;
1593
                    }
1594
                }
1595
                if ($canAccess) {
1596
                    $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
1597
                    return isset($this->accepted_compression);
1598
                }
1599
                // break through voluntarily
1600
            default:
1601
                return false;
1602
        }
1603
    }
1604
1605
    public function __unset($name)
1606
    {
1607
        switch ($name) {
1608
            case self::OPT_ACCEPTED_COMPRESSION :
1609
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1610
            case self::OPT_COMPRESS_RESPONSE:
1611
            case self::OPT_DEBUG:
1612
            case self::OPT_EXCEPTION_HANDLING:
1613
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1614
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1615
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1616
                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
1617
                unset($this->$name);
1618
                break;
1619
            case 'accepted_charset_encodings':
1620
                // manually implement the 'protected property' behaviour
1621
                $canAccess = false;
1622
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1623
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1624
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1625
                        $canAccess = true;
1626
                    }
1627
                }
1628
                if ($canAccess) {
1629
                    $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
1630
                    unset($this->accepted_compression);
1631
                } else {
1632
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1633
                }
1634
                break;
1635
            default:
1636
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1637
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1638
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1639
        }
1640
    }
1641
}
1642