Issues (323)

src/Server.php (16 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
        $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);
0 ignored issues
show
It seems like $parametersType can also be of type false; however, parameter $parametersType of PhpXmlRpc\Server::add_to_map() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

425
       $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, /** @scrutinizer ignore-type */ $parametersType, $exceptionHandling);
Loading history...
It seems like $exceptionHandling can also be of type false; however, parameter $exceptionHandling of PhpXmlRpc\Server::add_to_map() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

425
       $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, /** @scrutinizer ignore-type */ $exceptionHandling);
Loading history...
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

425
       /** @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...
It seems like $sigDoc can also be of type false; however, parameter $sigDoc of PhpXmlRpc\Server::add_to_map() does only seem to accept array<mixed,array>, maybe add an additional type check? ( Ignorable by Annotation )

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

425
       $this->add_to_map($methodName, $function, $sig, $doc, /** @scrutinizer ignore-type */ $sigDoc, $parametersType, $exceptionHandling);
Loading history...
It seems like $doc can also be of type false; however, parameter $doc of PhpXmlRpc\Server::add_to_map() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

425
       $this->add_to_map($methodName, $function, $sig, /** @scrutinizer ignore-type */ $doc, $sigDoc, $parametersType, $exceptionHandling);
Loading history...
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}");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $got does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $pno does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
            // unless we are in "interop faults" mode
684 150
            preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
685 24
            return new static::$responseClass(
686
                0,
687 127
                (PhpXmlRpc::isUsingInteropFaults() ? PhpXmlRpc::$xmlrpcerr['invalid_xml'] : PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1]),
688
                $_xh['isf_reason']);
689 431
        } elseif ($_xh['isf']) {
690 129
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
691
            ///       parsing error
692 324
            return new static::$responseClass(
693
                0,
694
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
695
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
696 559
        } else {
697
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
698
            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
699
            // that would mean a useless encode+decode pass
700
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
701
                (isset($this->dmap[$_xh['method']]['parameters_type']) &&
702
                    ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
703
                )
704
            ) {
705
                if ($this->debug > 1) {
706
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
707 559
                }
708 559
709
                return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
710
            } else {
711
                // build a Request object with data parsed from xml and add parameters in
712
                $req = new Request($_xh['method']);
713 559
                /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)...
714 559
                for ($i = 0; $i < count($_xh['params']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
715 127
                    $req->addParam($_xh['params'][$i]);
716
                }
717 454
718
                if ($this->debug > 1) {
719 557
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
720
                }
721
722
                return $this->execute($req);
723
            }
724
        }
725
    }
726
727
    /**
728
     * Execute a method invoked by the client, checking parameters used.
729
     *
730
     * @param Request|string $req either a Request obj or a method name
731
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
732
     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
733
     * @return Response
734
     *
735
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
736
     */
737
    protected function execute($req, $params = null, $paramTypes = null)
738
    {
739
        static::$_xmlrpcs_occurred_errors = '';
740
        static::$_xmlrpc_debuginfo = '';
741
742
        if (is_object($req)) {
743
            $methodName = $req->method();
744
        } else {
745
            $methodName = $req;
746
        }
747
748
        $sysCall = $this->isSyscall($methodName);
749
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
750
751
        if (!isset($dmap[$methodName]['function'])) {
752
            // No such method
753
            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
754
        }
755
756
        // Check signature
757
        if (isset($dmap[$methodName]['signature'])) {
758
            $sig = $dmap[$methodName]['signature'];
759
            if (is_object($req)) {
760
                list($ok, $errStr) = $this->verifySignature($req, $sig);
761
            } else {
762 45
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
763
            }
764
            if (!$ok) {
765 45
                // Didn't match.
766 45
                return new static::$responseClass(
767
                    0,
768
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
769
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
770
                );
771
            }
772
        }
773
774
        $func = $dmap[$methodName]['function'];
775 45
776 2
        // let the 'class::function' syntax be accepted in dispatch maps
777 2
        if (is_string($func) && strpos($func, '::')) {
778
            $func = explode('::', $func);
779 45
        }
780
781
        // build string representation of function 'name'
782 559
        if (is_array($func)) {
783
            if (is_object($func[0])) {
784
                $funcName = get_class($func[0]) . '->' . $func[1];
785 559
            } else {
786 64
                $funcName = implode('::', $func);
787
            }
788 496
        } else if ($func instanceof \Closure) {
789
            $funcName = 'Closure';
790
        } else {
791
            $funcName = $func;
792 559
        }
793
794
        // verify that function to be invoked is in fact callable
795
        if (!is_callable($func)) {
796
            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
797
            return new static::$responseClass(
798
                0,
799
                PhpXmlRpc::$xmlrpcerr['server_error'],
800 559
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
801
            );
802 559
        }
803 559
804
        if (isset($dmap[$methodName]['exception_handling'])) {
805
            $exception_handling = (int)$dmap[$methodName]['exception_handling'];
806
        } else {
807
            $exception_handling = $this->exception_handling;
808
        }
809 561
810
        // We always catch all errors generated during processing of user function, and log them as part of response;
811 561
        // if debug level is 3 or above, we also serialize them in the response as comments
812 52
        self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
813
814 509
        /// @todo what about using output-buffering as well, in case user code echoes anything to screen?
815
816
        try {
817
            // Allow mixed-convention servers
818
            if (is_object($req)) {
819
                // call an 'xml-rpc aware' function
820
                if ($sysCall) {
821
                    $r = call_user_func($func, $this, $req);
822 559
                } else {
823
                    $r = call_user_func($func, $req);
824 559
                }
825
                if (!is_a($r, 'PhpXmlRpc\Response')) {
826
                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
827
                    if (is_a($r, 'PhpXmlRpc\Value')) {
828
                        $r = new static::$responseClass($r);
829
                    } else {
830
                        $r = new static::$responseClass(
831
                            0,
832
                            PhpXmlRpc::$xmlrpcerr['server_error'],
833
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
834
                        );
835
                    }
836
                }
837
            } else {
838 127
                // call a 'plain php' function
839
                if ($sysCall) {
840 127
                    array_unshift($params, $this);
0 ignored issues
show
It seems like $params can also be of type null; however, parameter $array of array_unshift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

840
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
841
                    $r = call_user_func_array($func, $params);
842
                } else {
843
                    // 3rd API convention for method-handling functions: EPI-style
844
                    if ($this->functions_parameters_type == 'epivals') {
845
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
846 127
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
847
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
848
                            $r = new static::$responseClass(0, (int)$r['faultCode'], (string)$r['faultString']);
849 127
                        } else {
850 127
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
851
                            // return type correctly
852
                            $encoder = new Encoder();
853
                            $r = new static::$responseClass($encoder->encode($r, array('extension_api')));
854 127
                        }
855 127
                    } else {
856 127
                        $r = call_user_func_array($func, $params);
0 ignored issues
show
It seems like $params can also be of type null; however, parameter $args of call_user_func_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

856
                        $r = call_user_func_array($func, /** @scrutinizer ignore-type */ $params);
Loading history...
857
                    }
858
                }
859
                // the return type can be either a Response object or a plain php value...
860 127
                if (!is_a($r, '\PhpXmlRpc\Response')) {
861 127
                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
862 127
                    // a: let the user decide
863
                    $encoder = new Encoder();
864
                    $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options));
865
                }
866 127
            }
867 127
        /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
868 127
        } catch (\Exception $e) {
869
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
870
            // proper error-response
871
            switch ($exception_handling) {
872 127
                case 2:
873 127
                    if (self::$_xmlrpcs_prev_ehandler) {
874 127
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
875
                        self::$_xmlrpcs_prev_ehandler = null;
876
                    } else {
877
                        restore_error_handler();
878
                    }
879
                    throw $e;
880
                case 1:
881
                    $errCode = $e->getCode();
882
                    if ($errCode == 0) {
883
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
884
                    }
885
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
886
                    break;
887
                default:
888
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
889
            }
890
        } catch (\Error $e) {
891
            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
892
            // proper error-response
893
            switch ($exception_handling) {
894
                case 2:
895
                    if (self::$_xmlrpcs_prev_ehandler) {
896
                        set_error_handler(self::$_xmlrpcs_prev_ehandler);
897
                        self::$_xmlrpcs_prev_ehandler = null;
898
                    } else {
899
                        restore_error_handler();
900
                    }
901
                    throw $e;
902
                case 1:
903
                    $errCode = $e->getCode();
904
                    if ($errCode == 0) {
905
                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
906
                    }
907
                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
908
                    break;
909
                default:
910
                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
911
            }
912
        }
913
914
        // note: restore the error handler we found before calling the user func, even if it has been changed
915
        // inside the func itself
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
923
        return $r;
924
    }
925
926
    /**
927
     * @param Response $resp
928
     * @param string $respCharset
929
     * @return string
930
     */
931
    protected function generatePayload($resp, $respCharset)
932
    {
933 22
        $header = $resp->xml_header($respCharset);
934
        if ($this->debug > 0) {
935 22
            $header .= $this->serializeDebug($respCharset);
936 22
        }
937 22
938
        // Do not create response serialization if it has already happened. Helps to build json magic
939 22
        /// @todo what if the payload was created targeting a different charset than $respCharset?
940 22
        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
941
        $payload = $resp->getPayload();
942
        if (empty($payload)) {
943 22
            $payload = $resp->serialize($respCharset);
944
        }
945
946
        return $header . $payload;
947
    }
948
949
    /**
950
     * @param string $payload
951 106
     * @param string $respContentType
952
     * @param string $respEncoding
953
     * @return void
954 106
     */
955 106
    protected function printPayload($payload, $respContentType, $respEncoding)
956 106
    {
957
        // if we get a warning/error that has output some text before here, then we cannot
958
        // add a new header. We cannot say we are sending xml, either...
959
        if (!headers_sent()) {
960 106
            header('Content-Type: ' . $respContentType);
961 85
            // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
962
            header("Vary: Accept-Charset");
963 22
964
            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
965 106
            // and php ini settings do not force it already
966 106
            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
967 106
            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
968 106
                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
969 106
                    $payload = gzencode($payload);
970 106
                    header("Content-Encoding: gzip");
971 106
                    header("Vary: Accept-Encoding");
972
                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
973 106
                    $payload = gzcompress($payload);
974
                    header("Content-Encoding: deflate");
975 106
                    header("Vary: Accept-Encoding");
976
                }
977
            }
978
979
            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
980
            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
981
            // responses up to 8000 bytes
982 1
            if ($phpNoSelfCompress) {
983
                header('Content-Length: ' . (int)strlen($payload));
984
            }
985 106
        } else {
986
            /// @todo allow the user to easily subclass this in a way which allows the resp. headers to be already sent
987
            ///       by now without flagging it as an error. Possibly check for presence of Content-Type header
988
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
989
        }
990
991
        print $payload;
992
    }
993 85
994
    /**
995
     * Registered as callback for when the XMLParser has found the name of the method to execute.
996 85
     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
997 85
     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
998 85
     *
999
     * @internal
1000
     * @param $methodName
1001
     * @param XMLParser $xmlParser
1002 85
     * @param null|resource $parser
1003 85
     * @return void
1004
     * @throws NoSuchMethodException
1005 1
     *
1006
     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
1007 85
     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
1008 85
     *       about the matched method handler, in order to avoid doing the work twice...
1009 85
     */
1010
    public function methodNameCallback($methodName, $xmlParser, $parser = null)
1011
    {
1012
        $sysCall = $this->isSyscall($methodName);
1013
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
1014
1015
        if (!isset($dmap[$methodName]['function'])) {
1016
            // No such method
1017 85
            throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
1018
        }
1019
1020 64
        // alter on-the-fly the config of the xml parser if needed
1021
        if (isset($dmap[$methodName]['parameters_type']) &&
1022 64
            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
1023 64
            /// @todo this should be done by a method of the XMLParser
1024 64
            switch ($dmap[$methodName]['parameters_type']) {
1025
                case XMLParser::RETURN_PHP:
1026 64
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee_fast'));
1027 64
                    break;
1028
                case XMLParser::RETURN_EPIVALS:
1029 64
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee_epi'));
1030 64
                    break;
1031 64
                /// @todo log a warning on unsupported return type
1032
                case XMLParser::RETURN_XMLRPCVALS:
1033 64
                default:
1034
                    xml_set_element_handler($parser, array($xmlParser, 'xmlrpc_se'), array($xmlParser, 'xmlrpc_ee'));
1035
            }
1036
        }
1037
    }
1038
1039
    /**
1040
     * Add a string to the 'internal debug message' (separate from 'user debug message').
1041 64
     *
1042
     * @param string $string
1043 64
     * @return void
1044
     */
1045
    protected function debugMsg($string)
1046 64
    {
1047 64
        $this->debug_info .= $string . "\n";
1048
    }
1049
1050 64
    /**
1051
     * @param string $methName
1052
     * @return bool
1053 64
     */
1054 64
    protected function isSyscall($methName)
1055
    {
1056
        return (strpos($methName, "system.") === 0);
1057 64
    }
1058 64
1059
    /**
1060
     * @param array $dmap
1061 64
     * @return $this
1062
     */
1063
    public function setDispatchMap($dmap)
1064
    {
1065 64
        $this->dmap = $dmap;
1066 64
        return $this;
1067 64
    }
1068
1069
    /**
1070
     * @return array[]
1071
     */
1072
    public function getDispatchMap()
1073
    {
1074
        return $this->dmap;
1075 64
    }
1076
1077 64
    /**
1078 64
     * @return array[]
1079
     */
1080
    public function getSystemDispatchMap()
1081 64
    {
1082
        if (!$this->allow_system_funcs) {
1083
            return array();
1084
        }
1085
1086
        return array(
1087
            'system.listMethods' => array(
1088
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
1089
                // listMethods: signature was either a string, or nothing.
1090
                // The useless string variant has been removed
1091
                'signature' => array(array(Value::$xmlrpcArray)),
1092
                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
1093
                'signature_docs' => array(array('list of method names')),
1094
            ),
1095
            'system.methodHelp' => array(
1096
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
1097
                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
1098
                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
1099
                'signature_docs' => array(array('method description', 'name of the method to be described')),
1100
            ),
1101
            'system.methodSignature' => array(
1102
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
1103
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
1104
                '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)',
1105
                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
1106
            ),
1107
            'system.multicall' => array(
1108
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
1109
                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
1110
                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
1111
                '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"')),
1112
            ),
1113
            'system.getCapabilities' => array(
1114
                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
1115
                'signature' => array(array(Value::$xmlrpcStruct)),
1116
                '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',
1117
                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
1118
            ),
1119
        );
1120
    }
1121
1122
    /**
1123
     * @return array[]
1124
     */
1125
    public function getCapabilities()
1126
    {
1127
        $outAr = array(
1128
            // xml-rpc spec: always supported
1129
            'xmlrpc' => array(
1130
                // NB: the spec sits now at https://xmlrpc.com/spec.md
1131
                'specUrl' => 'http://www.xmlrpc.com/spec',
1132
                'specVersion' => 1
1133
            ),
1134
            // if we support system.xxx functions, we always support multicall, too...
1135
            'system.multicall' => array(
1136
                // Note that, as of 2006/09/17, the following URL does not respond anymore
1137 85
                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
1138
                'specVersion' => 1
1139 85
            ),
1140
            // introspection: version 2! we support 'mixed', too.
1141 85
            // note: the php xml-rpc extension says this instead:
1142 85
            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
1143 85
            'introspection' => array(
1144 64
                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
1145
                'specVersion' => 2,
1146
            ),
1147
        );
1148
1149
        // NIL extension
1150
        if (PhpXmlRpc::$xmlrpc_null_extension) {
1151
            $outAr['nil'] = array(
1152
                // Note that, as of 2023/01, the following URL does not respond anymore
1153 85
                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
1154
                'specVersion' => 1
1155
            );
1156
        }
1157
1158
        // support for "standard" error codes
1159
        if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
1160
            $outAr['faults_interop'] = array(
1161
                // Note that, as of 2025/10, the following URL does not respond anymore
1162
                'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
1163
                'specVersion' => 20010516
1164 43
            );
1165
        }
1166
1167 43
        return $outAr;
1168 22
    }
1169
1170
    /**
1171
     * @internal handler of a system. method
1172 22
     *
1173 22
     * @param Server $server
1174
     * @param Request $req
1175
     * @return Response
1176
     */
1177 22
    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

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

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