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');
0 ignored issues
show
Deprecated Code introduced by
The property PhpXmlRpc\Server::$accepted_charset_encodings has been deprecated: UNUSED so far by this library. It is still accessible by subclasses but will be dropped in the future. ( Ignorable by Annotation )

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

185
        /** @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...
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
        $header = $resp->xml_header($respCharset);
394 536
        if ($this->debug > 0) {
395 536
            $header .= $this->serializeDebug($respCharset);
396 536
        }
397 536
398 515
        // Do not create response serialization if it has already happened. Helps to build json magic
399 515
        /// @todo what if the payload was created targeting a different charset than $respCharset?
400 515
        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
401 452
        $payload = $resp->getPayload();
402
        if (empty($payload)) {
403 148
            $payload = $resp->serialize($respCharset);
404
        }
405
        $payload = $header . $payload;
406
407
        if ($returnPayload) {
408
            return $payload;
409
        }
410 515
411 22
        // if we get a warning/error that has output some text before here, then we cannot
412 22
        // add a new header. We cannot say we are sending xml, either...
413 22
        if (!headers_sent()) {
414 22
            header('Content-Type: ' . $resp->getContentType());
415 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
416
            header("Vary: Accept-Charset");
417
418 536
            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
419 536
            // and php ini settings do not force it already
420
            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
421
            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
422
                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
423 22
                    $payload = gzencode($payload);
424
                    header("Content-Encoding: gzip");
425
                    header("Vary: Accept-Encoding");
426 22
                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
427
                    $payload = gzcompress($payload);
428
                    header("Content-Encoding: deflate");
429
                    header("Vary: Accept-Encoding");
430
                }
431
            }
432
433
            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
434
            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
435 561
            // responses up to 8000 bytes
436
            if ($phpNoSelfCompress) {
437
                header('Content-Length: ' . (int)strlen($payload));
438
            }
439 561
        } else {
440
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
441
        }
442
443 561
        print $payload;
444 559
445 559
        // return response, in case subclasses want it
446 559
        return $resp;
447 559
    }
448
449
    /**
450
     * Add a method to the dispatch map.
451
     *
452 561
     * @param string $methodName the name with which the method will be made available
453 104
     * @param callable $function the php function that will get invoked
454
     * @param array[] $sig the array of valid method signatures.
455 457
     *                     Each element is one signature: an array of strings with at least one element
456
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
457
     * @param string $doc method documentation
458 561
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
459
     *                        descriptions instead of types (one string for return type, one per param)
460
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
461 561
     * @param int $exceptionHandling @see $this->exception_handling
462 104
     * @return void
463
     *
464 104
     * @todo raise a warning if the user tries to register a 'system.' method
465 104
     */
466 52
    public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
467 52
        $exceptionHandling = false)
468 52
    {
469
       $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);
0 ignored issues
show
Deprecated Code introduced by
The function PhpXmlRpc\Server::add_to_map() has been deprecated: use addToMap instead ( Ignorable by Annotation )

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

469
       /** @scrutinizer ignore-deprecated */ $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);

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

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

Loading history...
470 52
    }
471 52
472 52
    /**
473 52
     * Add a method to the dispatch map.
474
     *
475
     * @param string $methodName the name with which the method will be made available
476
     * @param callable $function the php function that will get invoked
477
     * @param array[] $sig the array of valid method signatures.
478
     *                     Each element is one signature: an array of strings with at least one element
479
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
480
     * @param string $doc method documentation
481
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
482
     *                        descriptions instead of types (one string for return type, one per param)
483
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
484
     * @param int $exceptionHandling @see $this->exception_handling
485
     * @return void
486
     *
487
     * @todo raise a warning if the user tries to register a 'system.' method
488
     * @deprecated use addToMap instead
489
     */
490
    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
491
        $exceptionHandling = false)
492
    {
493
        $this->logDeprecationUnlessCalledBy('addToMap');
494 561
495
        $this->dmap[$methodName] = array(
496
            'function' => $function,
497
            'docstring' => $doc,
498
        );
499
        if ($sig) {
500
            $this->dmap[$methodName]['signature'] = $sig;
501
        }
502
        if ($sigDoc) {
503
            $this->dmap[$methodName]['signature_docs'] = $sigDoc;
504
        }
505
        if ($parametersType) {
506
            $this->dmap[$methodName]['parameters_type'] = $parametersType;
507
        }
508
        if ($exceptionHandling !== false) {
509
            $this->dmap[$methodName]['exception_handling'] = $exceptionHandling;
510
        }
511
    }
512
513
    /**
514
     * Verify type and number of parameters received against a list of known signatures.
515
     *
516 561
     * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
517
     * @param array $sigs array of known signatures to match against
518
     * @return array int, string
519 561
     */
520 104
    protected function verifySignature($in, $sigs)
521
    {
522 457
        // check each possible signature in turn
523
        if (is_object($in)) {
524
            $numParams = $in->getNumParams();
525
        } else {
526
            $numParams = count($in);
527 561
        }
528
        foreach ($sigs as $curSig) {
529
            if (count($curSig) == $numParams + 1) {
530 561
                $itsOK = 1;
531
                for ($n = 0; $n < $numParams; $n++) {
532
                    if (is_object($in)) {
533
                        $p = $in->getParam($n);
534
                        if ($p->kindOf() == 'scalar') {
535
                            $pt = $p->scalarTyp();
536
                        } else {
537
                            $pt = $p->kindOf();
538
                        }
539
                    } else {
540
                        $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
541
                    }
542
543
                    // param index is $n+1, as first member of sig is return type
544
                    if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
545
                        $itsOK = 0;
546
                        $pno = $n + 1;
547 562
                        $wanted = $curSig[$n + 1];
548
                        $got = $pt;
549
                        break;
550
                    }
551 562
                }
552
                if ($itsOK) {
553
                    return array(1, '');
554
                }
555
            }
556
        }
557
        if (isset($wanted)) {
558
            return array(0, "Wanted {$wanted}, got {$got} at param {$pno}");
559
        } else {
560 561
            return array(0, "No method signature matches number of parameters");
561 4
        }
562 2
    }
563
564 2
    /**
565 2
     * Parse http headers received along with xml-rpc request. If needed, inflate request.
566
     *
567
     * @return Response|null null on success or an error Response
568
     */
569
    protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
570
    {
571
        // check if $_SERVER is populated: it might have been disabled via ini file
572
        // (this is true even when in CLI mode)
573
        if (count($_SERVER) == 0) {
574
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
575
        }
576
577 562
        if ($this->debug > 1) {
578
            if (function_exists('getallheaders')) {
579
                $this->debugMsg(''); // empty line
580
                foreach (getallheaders() as $name => $val) {
581 562
                    $this->debugMsg("HEADER: $name: $val");
582
                }
583
            }
584 562
        }
585 562
586 562
        if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
587
            $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
588 2
        } else {
589 2
            $contentEncoding = '';
590 2
        }
591 2
592 560
        $rawData = $data;
593 1
594 1
        // check if request body has been compressed and decompress it
595 1
        if ($contentEncoding != '' && strlen($data)) {
596
            if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
597
                // if decoding works, use it. else assume data wasn't gzencoded
598
                /// @todo test separately for gzinflate and gzuncompress
599
                if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
600
                    if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
601 559
                        $data = $degzdata;
602 559
                        if ($this->debug > 1) {
603
                            $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
604
                        }
605
                    } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
606
                        $data = $degzdata;
607
                        if ($this->debug > 1) {
608
                            $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
609
                        }
610
                    } else {
611
                        $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
612 559
                            PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
613
                        );
614 559
615 538
                        return $r;
616
                    }
617
                } else {
618 559
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
619 559
                        PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
620
                    );
621 559
622
                    return $r;
623
                }
624
            }
625 562
        }
626
627
        // check if client specified accepted charsets, and if we know how to fulfill the request
628
        if ($this->response_charset_encoding == 'auto') {
629
            $respEncoding = '';
630
            if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
631
                // here we check if we can match the client-requested encoding with the encodings we know we can generate.
632
                // we parse q=0.x preferences instead of preferring the first charset specified
633
                $http = new Http();
634
                $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
635
                $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
636
                foreach ($clientAcceptedCharsets as $accepted) {
637
                    foreach ($knownCharsets as $charset) {
638
                        if (strtoupper($accepted) == strtoupper($charset)) {
639 559
                            $respEncoding = $charset;
640
                            break 2;
641 559
                        }
642 559
                    }
643
                }
644 559
            }
645 559
        } else {
646
            $respEncoding = $this->response_charset_encoding;
647
        }
648
649 559
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
650 559
            $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
651
        } else {
652 559
            $respCompression = '';
653
        }
654 85
655 85
        // 'guestimate' request encoding
656 85
        /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
657
        $parser = $this->getParser();
658
        $reqEncoding = $parser->guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
659
            $data);
660 559
661 536
        return null;
662 536
    }
663 536
664
    /**
665
     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
666
     * server.
667 536
     * @internal this function will become protected in the future
668
     *
669 22
     * @param string $data the xml request
670 22
     * @param string $reqEncoding (optional) the charset encoding of the xml request
671 22
     * @return Response
672 22
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
673
     *
674
     * @todo either rename this function or move the 'execute' part out of it...
675
     */
676
    public function parseRequest($data, $reqEncoding = '')
677 559
    {
678
        // decompose incoming XML into request structure
679 559
680 127
        /// @todo move this block of code into the XMLParser
681
        if ($reqEncoding != '') {
682
            // Since parsing will fail if
683 559
            // - charset is not specified in the xml declaration,
684 150
            // - the encoding is not UTF8 and
685 24
            // - there are non-ascii chars in the text,
686
            // we try to work round that...
687 127
            // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
688
            //if (!is_valid_charset($reqEncoding, array('UTF-8')))
689 431
            if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
690 129
                if (function_exists('mb_convert_encoding')) {
691
                    $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
692 324
                } else {
693
                    if ($reqEncoding == 'ISO-8859-1') {
694
                        $data = utf8_encode($data);
695
                    } else {
696 559
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
697
                    }
698
                }
699
            }
700
        }
701
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
702
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
703
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
704
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
705
        } else {
706
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
707 559
        }
708 559
        // register a callback with the xml parser for when it finds the method name
709
        $options['methodname_callback'] = array($this, 'methodNameCallback');
710
711
        $xmlRpcParser = $this->getParser();
712
        try {
713 559
            // NB: during parsing, the actual type of php values built will be automatically switched from
714 559
            // $this->functions_parameters_type to the one defined in the method signature, if defined there. This
715 127
            // happens via the parser making a call to $this->methodNameCallback as soon as it finds the desired method
716
            $_xh = $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
717 454
            // BC
718
            if (!is_array($_xh)) {
719 557
                $_xh = $xmlRpcParser->_xh;
720
            }
721
        } catch (NoSuchMethodException $e) {
722
            return new static::$responseClass(0, $e->getCode(), $e->getMessage());
723
        }
724
725
        if ($_xh['isf'] == 3) {
726
            // (BC) we return XML error as a faultCode
727
            preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
728
            return new static::$responseClass(
729
                0,
730
                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
731
                $_xh['isf_reason']);
732
        } elseif ($_xh['isf']) {
733
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
734
            ///       parsing error
735
            return new static::$responseClass(
736
                0,
737
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
738
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
739
        } else {
740
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
741
            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
742
            // that would mean a useless encode+decode pass
743
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
744
                (isset($this->dmap[$_xh['method']]['parameters_type']) &&
745
                    ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
746
                )
747
            ) {
748
                if ($this->debug > 1) {
749
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
750
                }
751
752
                return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
753
            } else {
754
                // build a Request object with data parsed from xml and add parameters in
755
                $req = new Request($_xh['method']);
756
                /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)...
757
                for ($i = 0; $i < count($_xh['params']); $i++) {
758
                    $req->addParam($_xh['params'][$i]);
759
                }
760
761
                if ($this->debug > 1) {
762 45
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
763
                }
764
765 45
                return $this->execute($req);
766 45
            }
767
        }
768
    }
769
770
    /**
771
     * Execute a method invoked by the client, checking parameters used.
772
     *
773
     * @param Request|string $req either a Request obj or a method name
774
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
775 45
     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
776 2
     * @return Response
777 2
     *
778
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
779 45
     */
780
    protected function execute($req, $params = null, $paramTypes = null)
781
    {
782 559
        static::$_xmlrpcs_occurred_errors = '';
783
        static::$_xmlrpc_debuginfo = '';
784
785 559
        if (is_object($req)) {
786 64
            $methodName = $req->method();
787
        } else {
788 496
            $methodName = $req;
789
        }
790
791
        $sysCall = $this->isSyscall($methodName);
792 559
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
793
794
        if (!isset($dmap[$methodName]['function'])) {
795
            // No such method
796
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
797
        }
798
799
        // Check signature
800 559
        if (isset($dmap[$methodName]['signature'])) {
801
            $sig = $dmap[$methodName]['signature'];
802 559
            if (is_object($req)) {
803 559
                list($ok, $errStr) = $this->verifySignature($req, $sig);
804
            } else {
805
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
806
            }
807
            if (!$ok) {
808
                // Didn't match.
809 561
                return new static::$responseClass(
810
                    0,
811 561
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
812 52
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
813
                );
814 509
            }
815
        }
816
817
        $func = $dmap[$methodName]['function'];
818
819
        // let the 'class::function' syntax be accepted in dispatch maps
820
        if (is_string($func) && strpos($func, '::')) {
821
            $func = explode('::', $func);
822 559
        }
823
824 559
        // build string representation of function 'name'
825
        if (is_array($func)) {
826
            if (is_object($func[0])) {
827
                $funcName = get_class($func[0]) . '->' . $func[1];
828
            } else {
829
                $funcName = implode('::', $func);
830
            }
831
        } else if ($func instanceof \Closure) {
832
            $funcName = 'Closure';
833
        } else {
834
            $funcName = $func;
835
        }
836
837
        // verify that function to be invoked is in fact callable
838 127
        if (!is_callable($func)) {
839
            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
840 127
            return new static::$responseClass(
841
                0,
842
                PhpXmlRpc::$xmlrpcerr['server_error'],
843
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
844
            );
845
        }
846 127
847
        if (isset($dmap[$methodName]['exception_handling'])) {
848
            $exception_handling = (int)$dmap[$methodName]['exception_handling'];
849 127
        } else {
850 127
            $exception_handling = $this->exception_handling;
851
        }
852
853
        // We always catch all errors generated during processing of user function, and log them as part of response;
854 127
        // if debug level is 3 or above, we also serialize them in the response as comments
855 127
        self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
856 127
857
        /// @todo what about using output-buffering as well, in case user code echoes anything to screen?
858
859
        try {
860 127
            // Allow mixed-convention servers
861 127
            if (is_object($req)) {
862 127
                // call an 'xml-rpc aware' function
863
                if ($sysCall) {
864
                    $r = call_user_func($func, $this, $req);
865
                } else {
866 127
                    $r = call_user_func($func, $req);
867 127
                }
868 127
                if (!is_a($r, 'PhpXmlRpc\Response')) {
869
                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
870
                    if (is_a($r, 'PhpXmlRpc\Value')) {
871
                        $r = new static::$responseClass($r);
872 127
                    } else {
873 127
                        $r = new static::$responseClass(
874 127
                            0,
875
                            PhpXmlRpc::$xmlrpcerr['server_error'],
876
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
877
                        );
878
                    }
879
                }
880
            } else {
881
                // call a 'plain php' function
882
                if ($sysCall) {
883
                    array_unshift($params, $this);
884
                    $r = call_user_func_array($func, $params);
885
                } else {
886
                    // 3rd API convention for method-handling functions: EPI-style
887
                    if ($this->functions_parameters_type == 'epivals') {
888
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
889
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
890
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
891
                            $r = new static::$responseClass(0, (integer)$r['faultCode'], (string)$r['faultString']);
892
                        } else {
893
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
894
                            // return type correctly
895
                            $encoder = new Encoder();
896
                            $r = new static::$responseClass($encoder->encode($r, array('extension_api')));
897
                        }
898
                    } else {
899
                        $r = call_user_func_array($func, $params);
900
                    }
901
                }
902
                // the return type can be either a Response object or a plain php value...
903
                if (!is_a($r, '\PhpXmlRpc\Response')) {
904
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
905
                    // a: let the user decide
906
                    $encoder = new Encoder();
907
                    $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options));
908
                }
909
            }
910
        /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
911
        } catch (\Exception $e) {
912
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
913
            // proper error-response
914
            switch ($exception_handling) {
915
                case 2:
916
                    if (self::$_xmlrpcs_prev_ehandler) {
917
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
918
                        self::$_xmlrpcs_prev_ehandler = null;
919
                    } else {
920
                        restore_error_handler();
921
                    }
922
                    throw $e;
923
                case 1:
924
                    $errCode = $e->getCode();
925
                    if ($errCode == 0) {
926
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
927
                    }
928
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
929
                    break;
930
                default:
931
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
932
            }
933 22
        } catch (\Error $e) {
934
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
935 22
            // proper error-response
936 22
            switch ($exception_handling) {
937 22
                case 2:
938
                    if (self::$_xmlrpcs_prev_ehandler) {
939 22
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
940 22
                        self::$_xmlrpcs_prev_ehandler = null;
941
                    } else {
942
                        restore_error_handler();
943 22
                    }
944
                    throw $e;
945
                case 1:
946
                    $errCode = $e->getCode();
947
                    if ($errCode == 0) {
948
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
949
                    }
950
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
951 106
                    break;
952
                default:
953
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
954 106
            }
955 106
        }
956 106
957
        // note: restore the error handler we found before calling the user func, even if it has been changed
958
        // inside the func itself
959
        if (self::$_xmlrpcs_prev_ehandler) {
960 106
            set_error_handler(self::$_xmlrpcs_prev_ehandler);
961 85
            self::$_xmlrpcs_prev_ehandler = null;
962
        } else {
963 22
            restore_error_handler();
964
        }
965 106
966 106
        return $r;
967 106
    }
968 106
969 106
    /**
970 106
     * Registered as callback for when the XMLParser has found the name of the method to execute.
971 106
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
972
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
973 106
     *
974
     * @internal
975 106
     * @param $methodName
976
     * @param XMLParser $xmlParser
977
     * @param null|resource $parser
978
     * @return void
979
     * @throws NoSuchMethodException
980
     *
981
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
982 1
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
983
     *       about the matched method handler, in order to avoid doing the work twice...
984
     */
985 106
    public function methodNameCallback($methodName, $xmlParser, $parser = null)
986
    {
987
        $sysCall = $this->isSyscall($methodName);
988
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
989
990
        if (!isset($dmap[$methodName]['function'])) {
991
            // No such method
992
            throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
993 85
        }
994
995
        // alter on-the-fly the config of the xml parser if needed
996 85
        if (isset($dmap[$methodName]['parameters_type']) &&
997 85
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
998 85
            /// @todo this should be done by a method of the XMLParser
999
            switch ($dmap[$methodName]['parameters_type']) {
1000
                case XMLParser::RETURN_PHP:
1001
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
1002 85
                    break;
1003 85
                case XMLParser::RETURN_EPIVALS:
1004
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
1005 1
                    break;
1006
                /// @todo log a warning on unsupported return type
1007 85
                case XMLParser::RETURN_XMLRPCVALS:
1008 85
                default:
1009 85
                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
1010
            }
1011
        }
1012
    }
1013
1014
    /**
1015
     * Add a string to the 'internal debug message' (separate from 'user debug message').
1016
     *
1017 85
     * @param string $string
1018
     * @return void
1019
     */
1020 64
    protected function debugMsg($string)
1021
    {
1022 64
        $this->debug_info .= $string . "\n";
1023 64
    }
1024 64
1025
    /**
1026 64
     * @param string $methName
1027 64
     * @return bool
1028
     */
1029 64
    protected function isSyscall($methName)
1030 64
    {
1031 64
        return (strpos($methName, "system.") === 0);
1032
    }
1033 64
1034
    /**
1035
     * @param array $dmap
1036
     * @return $this
1037
     */
1038
    public function setDispatchMap($dmap)
1039
    {
1040
        $this->dmap = $dmap;
1041 64
        return $this;
1042
    }
1043 64
1044
    /**
1045
     * @return array[]
1046 64
     */
1047 64
    public function getDispatchMap()
1048
    {
1049
        return $this->dmap;
1050 64
    }
1051
1052
    /**
1053 64
     * @return array[]
1054 64
     */
1055
    public function getSystemDispatchMap()
1056
    {
1057 64
        if (!$this->allow_system_funcs) {
1058 64
            return array();
1059
        }
1060
1061 64
        return array(
1062
            'system.listMethods' => array(
1063
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
1064
                // listMethods: signature was either a string, or nothing.
1065 64
                // The useless string variant has been removed
1066 64
                'signature' => array(array(Value::$xmlrpcArray)),
1067 64
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
1068
                'signature_docs' => array(array('list of method names')),
1069
            ),
1070
            'system.methodHelp' => array(
1071
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
1072
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
1073
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
1074
                'signature_docs' => array(array('method description', 'name of the method to be described')),
1075 64
            ),
1076
            'system.methodSignature' => array(
1077 64
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
1078 64
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
1079
                '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)',
1080
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
1081 64
            ),
1082
            'system.multicall' => array(
1083
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
1084
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
1085
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
1086
                '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"')),
1087
            ),
1088
            'system.getCapabilities' => array(
1089
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
1090
                'signature' => array(array(Value::$xmlrpcStruct)),
1091
                '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',
1092
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
1093
            ),
1094
        );
1095
    }
1096
1097
    /**
1098
     * @return array[]
1099
     */
1100
    public function getCapabilities()
1101
    {
1102
        $outAr = array(
1103
            // xml-rpc spec: always supported
1104
            'xmlrpc' => array(
1105
                'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md
1106
                'specVersion' => 1
1107
            ),
1108
            // if we support system.xxx functions, we always support multicall, too...
1109
            'system.multicall' => array(
1110
                // Note that, as of 2006/09/17, the following URL does not respond anymore
1111
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
1112
                'specVersion' => 1
1113
            ),
1114
            // introspection: version 2! we support 'mixed', too.
1115
            // note: the php xml-rpc extension says this instead:
1116
            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
1117
            'introspection' => array(
1118
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
1119
                'specVersion' => 2,
1120
            ),
1121
        );
1122
1123
        // NIL extension
1124
        if (PhpXmlRpc::$xmlrpc_null_extension) {
1125
            $outAr['nil'] = array(
1126
                // Note that, as of 2023/01, the following URL does not respond anymore
1127
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
1128
                'specVersion' => 1
1129
            );
1130
        }
1131
1132
        // support for "standard" error codes
1133
        if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
1134
            $outAr['faults_interop'] = array(
1135
                'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
1136
                'specVersion' => 20010516
1137 85
            );
1138
        }
1139 85
1140
        return $outAr;
1141 85
    }
1142 85
1143 85
    /**
1144 64
     * @internal handler of a system. method
1145
     *
1146
     * @param Server $server
1147
     * @param Request $req
1148
     * @return Response
1149
     */
1150
    public static function _xmlrpcs_getCapabilities($server, $req = null)
1151
    {
1152
        $encoder = new Encoder();
1153 85
        return new static::$responseClass($encoder->encode($server->getCapabilities()));
1154
    }
1155
1156
    /**
1157
     * @internal handler of a system. method
1158
     *
1159
     * @param Server $server
1160
     * @param Request $req if called in plain php values mode, second param is missing
1161
     * @return Response
1162
     */
1163
    public static function _xmlrpcs_listMethods($server, $req = null)
1164 43
    {
1165
        $outAr = array();
1166
        foreach ($server->dmap as $key => $val) {
1167 43
            $outAr[] = new Value($key, 'string');
1168 22
        }
1169
        foreach ($server->getSystemDispatchMap() as $key => $val) {
1170
            $outAr[] = new Value($key, 'string');
1171
        }
1172 22
1173 22
        return new static::$responseClass(new Value($outAr, 'array'));
1174
    }
1175
1176
    /**
1177 22
     * @internal handler of a system. method
1178
     *
1179
     * @param Server $server
1180 22
     * @param Request $req
1181 22
     * @return Response
1182 22
     */
1183
    public static function _xmlrpcs_methodSignature($server, $req)
1184 22
    {
1185
        // let's accept as parameter either an xml-rpc value or string
1186
        if (is_object($req)) {
1187
            $methName = $req->getParam(0);
1188
            $methName = $methName->scalarVal();
1189
        } else {
1190
            $methName = $req;
1191
        }
1192
        if ($server->isSyscall($methName)) {
1193
            $dmap = $server->getSystemDispatchMap();
1194
        } else {
1195
            $dmap = $server->dmap;
1196
        }
1197
        if (isset($dmap[$methName])) {
1198 22
            if (isset($dmap[$methName]['signature'])) {
1199
                $sigs = array();
1200
                foreach ($dmap[$methName]['signature'] as $inSig) {
1201
                    $curSig = array();
1202
                    foreach ($inSig as $sig) {
1203
                        $curSig[] = new Value($sig, 'string');
1204
                    }
1205
                    $sigs[] = new Value($curSig, 'array');
1206
                }
1207
                $r = new static::$responseClass(new Value($sigs, 'array'));
1208
            } else {
1209
                // NB: according to the official docs, we should be returning a
1210
                // "none-array" here, which means not-an-array
1211
                $r = new static::$responseClass(new Value('undef', 'string'));
1212
            }
1213
        } else {
1214
            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1215
        }
1216
1217
        return $r;
1218
    }
1219
1220
    /**
1221
     * @internal handler of a system. method
1222
     *
1223
     * @param Server $server
1224
     * @param Request $req
1225
     * @return Response
1226
     */
1227
    public static function _xmlrpcs_methodHelp($server, $req)
1228
    {
1229
        // let's accept as parameter either an xml-rpc value or string
1230
        if (is_object($req)) {
1231
            $methName = $req->getParam(0);
1232
            $methName = $methName->scalarVal();
1233
        } else {
1234
            $methName = $req;
1235
        }
1236
        if ($server->isSyscall($methName)) {
1237
            $dmap = $server->getSystemDispatchMap();
1238
        } else {
1239
            $dmap = $server->dmap;
1240
        }
1241
        if (isset($dmap[$methName])) {
1242
            if (isset($dmap[$methName]['docstring'])) {
1243
                $r = new static::$responseClass(new Value($dmap[$methName]['docstring'], 'string'));
1244
            } else {
1245
                $r = new static::$responseClass(new Value('', 'string'));
1246
            }
1247
        } else {
1248
            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1249
        }
1250
1251
        return $r;
1252
    }
1253
1254
    /**
1255
     * @internal this function will become protected in the future
1256
     *
1257
     * @param $err
1258
     * @return Value
1259
     */
1260
    public static function _xmlrpcs_multicall_error($err)
1261
    {
1262
        if (is_string($err)) {
1263
            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
1264
            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
1265
        } else {
1266
            $code = $err->faultCode();
1267
            $str = $err->faultString();
1268
        }
1269
        $struct = array();
1270
        $struct['faultCode'] = new Value($code, 'int');
1271
        $struct['faultString'] = new Value($str, 'string');
1272
1273
        return new Value($struct, 'struct');
1274
    }
1275
1276
    /**
1277
     * @internal this function will become protected in the future
1278
     *
1279
     * @param Server $server
1280
     * @param Value $call
1281
     * @return Value
1282
     */
1283
    public static function _xmlrpcs_multicall_do_call($server, $call)
1284
    {
1285
        if ($call->kindOf() != 'struct') {
1286
            return static::_xmlrpcs_multicall_error('notstruct');
1287
        }
1288
        $methName = @$call['methodName'];
1289
        if (!$methName) {
1290
            return static::_xmlrpcs_multicall_error('nomethod');
1291
        }
1292
        if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
1293
            return static::_xmlrpcs_multicall_error('notstring');
1294
        }
1295
        if ($methName->scalarVal() == 'system.multicall') {
1296
            return static::_xmlrpcs_multicall_error('recursion');
1297
        }
1298
1299
        $params = @$call['params'];
1300
        if (!$params) {
1301
            return static::_xmlrpcs_multicall_error('noparams');
1302
        }
1303
        if ($params->kindOf() != 'array') {
1304
            return static::_xmlrpcs_multicall_error('notarray');
1305
        }
1306
1307
        $req = new Request($methName->scalarVal());
1308
        foreach ($params as $i => $param) {
1309
            if (!$req->addParam($param)) {
1310
                $i++; // for error message, we count params from 1
1311
                return static::_xmlrpcs_multicall_error(new static::$responseClass(0,
1312
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1313
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1314
            }
1315
        }
1316
1317
        $result = $server->execute($req);
1318
1319
        if ($result->faultCode() != 0) {
1320
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1321
        }
1322
1323
        return new Value(array($result->value()), 'array');
1324
    }
1325
1326
    /**
1327
     * @internal this function will become protected in the future
1328
     *
1329
     * @param Server $server
1330
     * @param Value $call
1331
     * @return Value
1332
     */
1333
    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1334
    {
1335
        if (!is_array($call)) {
1336
            return static::_xmlrpcs_multicall_error('notstruct');
1337
        }
1338
        if (!array_key_exists('methodName', $call)) {
1339
            return static::_xmlrpcs_multicall_error('nomethod');
1340
        }
1341
        if (!is_string($call['methodName'])) {
1342
            return static::_xmlrpcs_multicall_error('notstring');
1343
        }
1344
        if ($call['methodName'] == 'system.multicall') {
1345
            return static::_xmlrpcs_multicall_error('recursion');
1346
        }
1347
        if (!array_key_exists('params', $call)) {
1348
            return static::_xmlrpcs_multicall_error('noparams');
1349
        }
1350
        if (!is_array($call['params'])) {
1351
            return static::_xmlrpcs_multicall_error('notarray');
1352
        }
1353
1354
        // this is a simplistic hack, since we might have received
1355
        // base64 or datetime values, but they will be listed as strings here...
1356
        $pt = array();
1357
        $wrapper = new Wrapper();
1358
        foreach ($call['params'] as $val) {
1359
            // support EPI-encoded base64 and datetime values
1360
            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
1361
                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
1362
            } else {
1363
                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
1364
            }
1365
        }
1366
1367
        $result = $server->execute($call['methodName'], $call['params'], $pt);
1368
1369
        if ($result->faultCode() != 0) {
1370
            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
1371
        }
1372
1373
        return new Value(array($result->value()), 'array');
1374
    }
1375
1376
    /**
1377
     * @internal handler of a system. method
1378
     *
1379
     * @param Server $server
1380
     * @param Request|array $req
1381
     * @return Response
1382
     */
1383
    public static function _xmlrpcs_multicall($server, $req)
1384
    {
1385
        $result = array();
1386
        // let's accept a plain list of php parameters, beside a single xml-rpc msg object
1387
        if (is_object($req)) {
1388
            $calls = $req->getParam(0);
1389
            foreach ($calls as $call) {
1390
                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
1391
            }
1392
        } else {
1393
            $numCalls = count($req);
1394
            for ($i = 0; $i < $numCalls; $i++) {
1395
                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
1396
            }
1397
        }
1398
1399
        return new static::$responseClass(new Value($result, 'array'));
1400
    }
1401
1402
    /**
1403
     * Error handler used to track errors that occur during server-side execution of PHP code.
1404
     * This allows to report back to the client whether an internal error has occurred or not
1405
     * using an xml-rpc response object, instead of letting the client deal with the html junk
1406
     * that a PHP execution error on the server generally entails.
1407
     *
1408
     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
1409
     *
1410
     * @internal
1411
     */
1412
    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
1413
    {
1414
        // obey the @ protocol
1415
        if (error_reporting() == 0) {
1416
            return;
1417
        }
1418
1419
        //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
1420
        if ($errCode != E_STRICT) {
1421
            static::error_occurred($errString);
1422
        }
1423
1424
        // Try to avoid as much as possible disruption to the previous error handling mechanism in place
1425
        if (self::$_xmlrpcs_prev_ehandler == '') {
1426
            // The previous error handler was the default: all we should do is log error to the default error log
1427
            // (if level high enough)
1428
            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
1429
                // we can't use the functionality of LoggerAware, because this is a static method
1430
                if (self::$logger === null) {
1431
                    self::$logger = Logger::instance();
1432
                }
1433
                self::$logger->error($errString);
1434
            }
1435
        } else {
1436
            // Pass control on to previous error handler, trying to avoid loops...
1437
            if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) {
1438
                if (is_array(self::$_xmlrpcs_prev_ehandler)) {
1439
                    // the following works both with static class methods and plain object methods as error handler
1440
                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
1441
                } else {
1442
                    $method = self::$_xmlrpcs_prev_ehandler;
1443
                    $method($errCode, $errString, $filename, $lineNo, $context);
1444
                }
1445
            }
1446
        }
1447
    }
1448
1449
    // *** BC layer ***
1450
1451
    /**
1452
     * @param string $charsetEncoding
1453
     * @return string
1454
     *
1455
     * @deprecated this method was moved to the Response class
1456
     */
1457
    protected function xml_header($charsetEncoding = '')
1458
    {
1459
        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
1460
1461
        if ($charsetEncoding != '') {
1462
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
1463
        } else {
1464
            return "<?xml version=\"1.0\"?" . ">\n";
1465
        }
1466
    }
1467
1468
    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
1469
    public function &__get($name)
1470
    {
1471
        switch ($name) {
1472
            case self::OPT_ACCEPTED_COMPRESSION :
1473
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1474
            case self::OPT_COMPRESS_RESPONSE:
1475
            case self::OPT_DEBUG:
1476
            case self::OPT_EXCEPTION_HANDLING:
1477
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1478
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1479
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1480
                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
1481
                return $this->$name;
1482
            case 'accepted_charset_encodings':
1483
                // manually implement the 'protected property' behaviour
1484
                $canAccess = false;
1485
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1486
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1487
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1488
                        $canAccess = true;
1489
                    }
1490
                }
1491
                if ($canAccess) {
1492
                    $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
1493
                    return $this->accepted_compression;
1494
                } else {
1495
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1496
                }
1497
                break;
1498
            default:
1499
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1500
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1501
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1502
                $result = null;
1503
                return $result;
1504
        }
1505
    }
1506
1507
    public function __set($name, $value)
1508
    {
1509
        switch ($name) {
1510
            case self::OPT_ACCEPTED_COMPRESSION :
1511
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1512
            case self::OPT_COMPRESS_RESPONSE:
1513
            case self::OPT_DEBUG:
1514
            case self::OPT_EXCEPTION_HANDLING:
1515
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1516
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1517
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1518
                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
1519
                $this->$name = $value;
1520
                break;
1521
            case 'accepted_charset_encodings':
1522
                // manually implement the 'protected property' behaviour
1523
                $canAccess = false;
1524
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1525
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1526
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1527
                        $canAccess = true;
1528
                    }
1529
                }
1530
                if ($canAccess) {
1531
                    $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
1532
                    $this->accepted_compression = $value;
1533
                } else {
1534
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1535
                }
1536
                break;
1537
            default:
1538
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1539
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1540
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1541
        }
1542
    }
1543
1544
    public function __isset($name)
1545
    {
1546
        switch ($name) {
1547
            case self::OPT_ACCEPTED_COMPRESSION :
1548
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1549
            case self::OPT_COMPRESS_RESPONSE:
1550
            case self::OPT_DEBUG:
1551
            case self::OPT_EXCEPTION_HANDLING:
1552
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1553
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1554
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1555
                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
1556
                return isset($this->$name);
1557
            case 'accepted_charset_encodings':
1558
                // manually implement the 'protected property' behaviour
1559
                $canAccess = false;
1560
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1561
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1562
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1563
                        $canAccess = true;
1564
                    }
1565
                }
1566
                if ($canAccess) {
1567
                    $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
1568
                    return isset($this->accepted_compression);
1569
                }
1570
                // break through voluntarily
1571
            default:
1572
                return false;
1573
        }
1574
    }
1575
1576
    public function __unset($name)
1577
    {
1578
        switch ($name) {
1579
            case self::OPT_ACCEPTED_COMPRESSION :
1580
            case self::OPT_ALLOW_SYSTEM_FUNCS:
1581
            case self::OPT_COMPRESS_RESPONSE:
1582
            case self::OPT_DEBUG:
1583
            case self::OPT_EXCEPTION_HANDLING:
1584
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
1585
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
1586
            case self::OPT_RESPONSE_CHARSET_ENCODING:
1587
                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
1588
                unset($this->$name);
1589
                break;
1590
            case 'accepted_charset_encodings':
1591
                // manually implement the 'protected property' behaviour
1592
                $canAccess = false;
1593
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
1594
                if (isset($trace[1]) && isset($trace[1]['class'])) {
1595
                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
1596
                        $canAccess = true;
1597
                    }
1598
                }
1599
                if ($canAccess) {
1600
                    $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
1601
                    unset($this->accepted_compression);
1602
                } else {
1603
                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
1604
                }
1605
                break;
1606
            default:
1607
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
1608
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
1609
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
1610
        }
1611
    }
1612
}
1613