Passed
Push — master ( 37c162...149546 )
by Gaetano
09:04
created

Server::debugMsg()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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

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

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

Loading history...
183
184
        // dispMap is a dispatch array of methods mapped to function names and signatures.
185
        // If a method doesn't appear in the map then an unknown method error is generated.
186
        // milosch - changed to make passing dispMap optional. Instead, you can use the class add_to_map() function
187
        // to add functions manually (borrowed from SOAPX4)
188
        if ($dispatchMap) {
189
            $this->setDispatchMap($dispatchMap);
190
            if ($serviceNow) {
191
                $this->service();
192 559
            }
193
        }
194 559
    }
195 559
196
    /**
197
     * @param string $name see all the OPT_ constants
198
     * @param mixed $value
199
     * @return $this
200
     * @throws ValueErrorException on unsupported option
201
     */
202
    public function setOption($name, $value)
203
    {
204 2
        switch ($name) {
205
            case self::OPT_ACCEPTED_COMPRESSION :
206 2
            case self::OPT_ALLOW_SYSTEM_FUNCS:
207 2
            case self::OPT_COMPRESS_RESPONSE:
208
            case self::OPT_DEBUG:
209
            case self::OPT_EXCEPTION_HANDLING:
210
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
211
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
212
            case self::OPT_RESPONSE_CHARSET_ENCODING:
213
                $this->$name = $value;
214
                break;
215
            default:
216 22
                throw new ValueErrorException("Unsupported option '$name'");
217
        }
218 22
219 22
        return $this;
220
    }
221
222
    /**
223
     * @param string $name see all the OPT_ constants
224
     * @return mixed
225
     * @throws ValueErrorException on unsupported option
226
     */
227
    public function getOption($name)
228 561
    {
229
        switch ($name) {
230
            case self::OPT_ACCEPTED_COMPRESSION:
231
            case self::OPT_ALLOW_SYSTEM_FUNCS:
232
            case self::OPT_COMPRESS_RESPONSE:
233
            case self::OPT_DEBUG:
234
            case self::OPT_EXCEPTION_HANDLING:
235 561
            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
236 561
            case self::OPT_PHPVALS_ENCODING_OPTIONS:
237 559
            case self::OPT_RESPONSE_CHARSET_ENCODING:
238
                return $this->$name;
239 561
            default:
240 2
                throw new ValueErrorException("Unsupported option '$name'");
241
        }
242
    }
243
244
    /**
245
     * Returns the complete list of Server options.
246 561
     * @return array
247
     */
248
    public function getOptions()
249
    {
250
        $values = array();
251
        foreach(static::$options as $opt) {
252
            $values[$opt] = $this->getOption($opt);
253
        }
254
        return $values;
255
    }
256
257
    /**
258
     * @param array $options key:  see all the OPT_ constants
259 561
     * @return $this
260
     * @throws ValueErrorException on unsupported option
261 561
     */
262 561
    public function setOptions($options)
263
    {
264 561
        foreach($options as $name => $value) {
265
            $this->setOption($name, $value);
266
        }
267 561
268
        return $this;
269
    }
270 561
271 559
    /**
272
     * Set debug level of server.
273
     *
274 561
     * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments)
275 561
     *                    0 = no debug info,
276
     *                    1 = msgs set from user with debugmsg(),
277 561
     *                    2 = add complete xml-rpc request (headers and body),
278
     *                    3 = add also all processing warnings happened during method processing
279
     *                    (NB: this involves setting a custom error handler, and might interfere
280
     *                    with the standard processing of the php function exposed as method. In
281
     *                    particular, triggering a USER_ERROR level error will not halt script
282 561
     *                    execution anymore, but just end up logged in the xml-rpc response)
283
     *                    Note that info added at level 2 and 3 will be base64 encoded
284
     * @return $this
285 561
     */
286 22
    public function setDebug($level)
287 22
    {
288
        $this->debug = $level;
289
        return $this;
290 561
    }
291 561
292 561
    /**
293
     * Add a string to the debug info that can be later serialized by the server as part of the response message.
294
     * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
295
     * character set.
296 561
     *
297 561
     * @param string $msg
298
     * @return void
299 561
     */
300
    public static function xmlrpc_debugmsg($msg)
301 561
    {
302
        static::$_xmlrpc_debuginfo .= $msg . "\n";
303
    }
304
305
    /**
306
     * Add a string to the debug info that will be later serialized by the server as part of the response message
307 561
     * (base64 encoded) when debug level >= 2
308 561
     *
309
     * @param string $msg
310
     * @return void
311 561
     */
312
    public static function error_occurred($msg)
313
    {
314
        static::$_xmlrpcs_occurred_errors .= $msg . "\n";
315
    }
316
317 561
    /**
318 561
     * Return a string with the serialized representation of all debug info.
319 104
     *
320
     * @internal this function will become protected in the future
321 104
     *
322 52
     * @param string $charsetEncoding the target charset encoding for the serialization
323 52
     *
324 52
     * @return string an XML comment (or two)
325 52
     */
326 52
    public function serializeDebug($charsetEncoding = '')
327 52
    {
328 52
        // Tough encoding problem: which internal charset should we assume for debug info?
329
        // It might contain a copy of raw data received from client, ie with unknown encoding,
330
        // intermixed with php generated data and user generated data...
331
        // so we split it: system debug is base 64 encoded,
332
        // user debug info should be encoded by the end user using the INTERNAL_ENCODING
333
        $out = '';
334
        if ($this->debug_info != '') {
335
            $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
336 561
        }
337 561
        if (static::$_xmlrpc_debuginfo != '') {
338
            $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
339
            // NB: a better solution MIGHT be to use CDATA, but we need to insert it
340
            // into return payload AFTER the beginning tag
341
            //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
342
        }
343 561
344
        return $out;
345
    }
346 561
347
    /**
348
     * Execute the xml-rpc request, printing the response.
349
     *
350
     * @param string $data the request body. If null, the http POST request will be examined
351
     * @param bool $returnPayload When true, return the response but do not echo it or any http header
352
     *
353
     * @return Response|string the response object (usually not used by caller...) or its xml serialization
354
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
355
     */
356
    public function service($data = null, $returnPayload = false)
357
    {
358
        if ($data === null) {
359
            $data = file_get_contents('php://input');
360
        }
361
        $rawData = $data;
362
363
        // reset internal debug info
364
        $this->debug_info = '';
365
366
        // Save what we received, before parsing it
367
        if ($this->debug > 1) {
368
            $this->debugMsg("+++GOT+++\n" . $data . "\n+++END+++");
369
        }
370
371
        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
372
        if (!$resp) {
373
            // this actually executes the request
374
            $resp = $this->parseRequest($data, $reqCharset);
375
376
            // save full body of request into response, for debugging purposes.
377
            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
378
            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
379
            ///       better: just avoid setting this, and set debug info of the received http request in the request
380
            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
381
            ///       Last but not least: the raw data might be of use to handler functions - but in decompressed form...
382
            $resp->raw_data = $rawData;
383
        }
384
385
        if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
386 536
            $this->debugMsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
387
                static::$_xmlrpcs_occurred_errors . "+++END+++");
388
        }
389 536
390 536
        $header = $resp->xml_header($respCharset);
391
        if ($this->debug > 0) {
392
            $header .= $this->serializeDebug($respCharset);
393
        }
394 536
395 536
        // Do not create response serialization if it has already happened. Helps to build json magic
396 536
        /// @todo what if the payload was created targeting a different charset than $respCharset?
397 536
        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
398 515
        $payload = $resp->getPayload();
399 515
        if (empty($payload)) {
400 515
            $payload = $resp->serialize($respCharset);
401 452
        }
402
        $payload = $header . $payload;
403 148
404
        if ($returnPayload) {
405
            return $payload;
406
        }
407
408
        // if we get a warning/error that has output some text before here, then we cannot
409
        // add a new header. We cannot say we are sending xml, either...
410 515
        if (!headers_sent()) {
411 22
            header('Content-Type: ' . $resp->getContentType());
412 22
            // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
413 22
            header("Vary: Accept-Charset");
414 22
415 22
            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
416
            // and php ini settings do not force it already
417
            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
418 536
            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
419 536
                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
420
                    $payload = gzencode($payload);
421
                    header("Content-Encoding: gzip");
422
                    header("Vary: Accept-Encoding");
423 22
                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
424
                    $payload = gzcompress($payload);
425
                    header("Content-Encoding: deflate");
426 22
                    header("Vary: Accept-Encoding");
427
                }
428
            }
429
430
            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
431
            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
432
            // responses up to 8000 bytes
433
            if ($phpNoSelfCompress) {
434
                header('Content-Length: ' . (int)strlen($payload));
435 561
            }
436
        } else {
437
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
438
        }
439 561
440
        print $payload;
441
442
        // return response, in case subclasses want it
443 561
        return $resp;
444 559
    }
445 559
446 559
    /**
447 559
     * Add a method to the dispatch map.
448
     *
449
     * @param string $methodName the name with which the method will be made available
450
     * @param callable $function the php function that will get invoked
451
     * @param array[] $sig the array of valid method signatures.
452 561
     *                     Each element is one signature: an array of strings with at least one element
453 104
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
454
     * @param string $doc method documentation
455 457
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
456
     *                        descriptions instead of types (one string for return type, one per param)
457
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
458 561
     * @param int $exceptionHandling @see $this->exception_handling
459
     * @return void
460
     *
461 561
     * @todo raise a warning if the user tries to register a 'system.' method
462 104
     */
463
    public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
464 104
        $exceptionHandling = false)
465 104
    {
466 52
       return $this->addToMap($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);
0 ignored issues
show
Bug introduced by
It seems like $parametersType can also be of type false; however, parameter $parametersType of PhpXmlRpc\Server::addToMap() 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

466
       return $this->addToMap($methodName, $function, $sig, $doc, $sigDoc, /** @scrutinizer ignore-type */ $parametersType, $exceptionHandling);
Loading history...
Bug introduced by
It seems like $exceptionHandling can also be of type false; however, parameter $exceptionHandling of PhpXmlRpc\Server::addToMap() 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

466
       return $this->addToMap($methodName, $function, $sig, $doc, $sigDoc, $parametersType, /** @scrutinizer ignore-type */ $exceptionHandling);
Loading history...
Bug introduced by
Are you sure the usage of $this->addToMap($methodN...pe, $exceptionHandling) targeting PhpXmlRpc\Server::addToMap() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
It seems like $sigDoc can also be of type false; however, parameter $sigDoc of PhpXmlRpc\Server::addToMap() 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

466
       return $this->addToMap($methodName, $function, $sig, $doc, /** @scrutinizer ignore-type */ $sigDoc, $parametersType, $exceptionHandling);
Loading history...
Bug introduced by
It seems like $doc can also be of type false; however, parameter $doc of PhpXmlRpc\Server::addToMap() 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

466
       return $this->addToMap($methodName, $function, $sig, /** @scrutinizer ignore-type */ $doc, $sigDoc, $parametersType, $exceptionHandling);
Loading history...
467 52
    }
468 52
469
    /**
470 52
     * Add a method to the dispatch map.
471 52
     *
472 52
     * @param string $methodName the name with which the method will be made available
473 52
     * @param callable $function the php function that will get invoked
474
     * @param array[] $sig the array of valid method signatures.
475
     *                     Each element is one signature: an array of strings with at least one element
476
     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
477
     * @param string $doc method documentation
478
     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
479
     *                        descriptions instead of types (one string for return type, one per param)
480
     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
481
     * @param int $exceptionHandling @see $this->exception_handling
482
     * @return void
483
     *
484
     * @todo raise a warning if the user tries to register a 'system.' method
485
     * @deprecated use addToMap instead
486
     */
487
    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
488
        $exceptionHandling = false)
489
    {
490
        $this->logDeprecationUnlessCalledBy('addToMap');
491
492
        $this->dmap[$methodName] = array(
493
            'function' => $function,
494 561
            'docstring' => $doc,
495
        );
496
        if ($sig) {
497
            $this->dmap[$methodName]['signature'] = $sig;
498
        }
499
        if ($sigDoc) {
500
            $this->dmap[$methodName]['signature_docs'] = $sigDoc;
501
        }
502
        if ($parametersType) {
503
            $this->dmap[$methodName]['parameters_type'] = $parametersType;
504
        }
505
        if ($exceptionHandling !== false) {
506
            $this->dmap[$methodName]['exception_handling'] = $exceptionHandling;
507
        }
508
    }
509
510
    /**
511
     * Verify type and number of parameters received against a list of known signatures.
512
     *
513
     * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
514
     * @param array $sigs array of known signatures to match against
515
     * @return array int, string
516 561
     */
517
    protected function verifySignature($in, $sigs)
518
    {
519 561
        // check each possible signature in turn
520 104
        if (is_object($in)) {
521
            $numParams = $in->getNumParams();
522 457
        } else {
523
            $numParams = count($in);
524
        }
525
        foreach ($sigs as $curSig) {
526
            if (count($curSig) == $numParams + 1) {
527 561
                $itsOK = 1;
528
                for ($n = 0; $n < $numParams; $n++) {
529
                    if (is_object($in)) {
530 561
                        $p = $in->getParam($n);
531
                        if ($p->kindOf() == 'scalar') {
532
                            $pt = $p->scalarTyp();
533
                        } else {
534
                            $pt = $p->kindOf();
535
                        }
536
                    } else {
537
                        $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
538
                    }
539
540
                    // param index is $n+1, as first member of sig is return type
541
                    if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
542
                        $itsOK = 0;
543
                        $pno = $n + 1;
544
                        $wanted = $curSig[$n + 1];
545
                        $got = $pt;
546
                        break;
547 562
                    }
548
                }
549
                if ($itsOK) {
550
                    return array(1, '');
551 562
                }
552
            }
553
        }
554
        if (isset($wanted)) {
555
            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...
556
        } else {
557
            return array(0, "No method signature matches number of parameters");
558
        }
559
    }
560 561
561 4
    /**
562 2
     * Parse http headers received along with xml-rpc request. If needed, inflate request.
563
     *
564 2
     * @return Response|null null on success or an error Response
565 2
     */
566
    protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
567
    {
568
        // check if $_SERVER is populated: it might have been disabled via ini file
569
        // (this is true even when in CLI mode)
570
        if (count($_SERVER) == 0) {
571
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
572
        }
573
574
        if ($this->debug > 1) {
575
            if (function_exists('getallheaders')) {
576
                $this->debugMsg(''); // empty line
577 562
                foreach (getallheaders() as $name => $val) {
578
                    $this->debugMsg("HEADER: $name: $val");
579
                }
580
            }
581 562
        }
582
583
        if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
584 562
            $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
585 562
        } else {
586 562
            $contentEncoding = '';
587
        }
588 2
589 2
        $rawData = $data;
590 2
591 2
        // check if request body has been compressed and decompress it
592 560
        if ($contentEncoding != '' && strlen($data)) {
593 1
            if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
594 1
                // if decoding works, use it. else assume data wasn't gzencoded
595 1
                /// @todo test separately for gzinflate and gzuncompress
596
                if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
597
                    if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
598
                        $data = $degzdata;
599
                        if ($this->debug > 1) {
600
                            $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
601 559
                        }
602 559
                    } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
603
                        $data = $degzdata;
604
                        if ($this->debug > 1) {
605
                            $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
606
                        }
607
                    } else {
608
                        $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
609
                            PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
610
                        );
611
612 559
                        return $r;
613
                    }
614 559
                } else {
615 538
                    $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
616
                        PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
617
                    );
618 559
619 559
                    return $r;
620
                }
621 559
            }
622
        }
623
624
        // check if client specified accepted charsets, and if we know how to fulfill the request
625 562
        if ($this->response_charset_encoding == 'auto') {
626
            $respEncoding = '';
627
            if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
628
                // here we check if we can match the client-requested encoding with the encodings we know we can generate.
629
                // we parse q=0.x preferences instead of preferring the first charset specified
630
                $http = new Http();
631
                $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
632
                $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
633
                foreach ($clientAcceptedCharsets as $accepted) {
634
                    foreach ($knownCharsets as $charset) {
635
                        if (strtoupper($accepted) == strtoupper($charset)) {
636
                            $respEncoding = $charset;
637
                            break 2;
638
                        }
639 559
                    }
640
                }
641 559
            }
642 559
        } else {
643
            $respEncoding = $this->response_charset_encoding;
644 559
        }
645 559
646
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
647
            $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
648
        } else {
649 559
            $respCompression = '';
650 559
        }
651
652 559
        // 'guestimate' request encoding
653
        /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
654 85
        $reqEncoding = XMLParser::guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
655 85
            $data);
656 85
657
        return null;
658
    }
659
660 559
    /**
661 536
     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
662 536
     * server.
663 536
     * @internal this function will become protected in the future
664
     *
665
     * @param string $data the xml request
666
     * @param string $reqEncoding (optional) the charset encoding of the xml request
667 536
     * @return Response
668
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
669 22
     *
670 22
     * @todo either rename this function or move the 'execute' part out of it...
671 22
     */
672 22
    public function parseRequest($data, $reqEncoding = '')
673
    {
674
        // decompose incoming XML into request structure
675
676
        /// @todo move this block of code into the XMLParser
677 559
        if ($reqEncoding != '') {
678
            // Since parsing will fail if
679 559
            // - charset is not specified in the xml declaration,
680 127
            // - the encoding is not UTF8 and
681
            // - there are non-ascii chars in the text,
682
            // we try to work round that...
683 559
            // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
684 150
            //if (!is_valid_charset($reqEncoding, array('UTF-8')))
685 24
            if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
686
                if (function_exists('mb_convert_encoding')) {
687 127
                    $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
688
                } else {
689 431
                    if ($reqEncoding == 'ISO-8859-1') {
690 129
                        $data = utf8_encode($data);
691
                    } else {
692 324
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
693
                    }
694
                }
695
            }
696 559
        }
697
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
698
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
699
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
700
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
701
        } else {
702
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
703
        }
704
        // register a callback with the xml parser for when it finds the method name
705
        $options['methodname_callback'] = array($this, 'methodNameCallback');
706
707 559
        $xmlRpcParser = $this->getParser();
708 559
        try {
709
            $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
710
            $_xh = $xmlRpcParser->_xh;
711
        } catch (NoSuchMethodException $e) {
712
            return new Response(0, $e->getCode(), $e->getMessage());
713 559
        }
714 559
715 127
        if ($_xh['isf'] == 3) {
716
            // (BC) we return XML error as a faultCode
717 454
            preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
718
            return new Response(
719 557
                0,
720
                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
721
                $_xh['isf_reason']);
722
        } elseif ($_xh['isf']) {
723
            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
724
            ///       parsing error
725
            return new Response(
726
                0,
727
                PhpXmlRpc::$xmlrpcerr['invalid_request'],
728
                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
729
        } else {
730
            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
731
            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
732
            // that would mean a useless encode+decode pass
733
            if ($this->functions_parameters_type != 'xmlrpcvals' ||
734
                (isset($this->dmap[$_xh['method']]['parameters_type']) &&
735
                    ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
736
                )
737
            ) {
738
                if ($this->debug > 1) {
739
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
740
                }
741
742
                return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
743
            } else {
744
                // build a Request object with data parsed from xml and add parameters in
745
                $req = new Request($_xh['method']);
746
                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...
747
                    $req->addParam($_xh['params'][$i]);
748
                }
749
750
                if ($this->debug > 1) {
751
                    $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
752
                }
753
754
                return $this->execute($req);
755
            }
756
        }
757
    }
758
759
    /**
760
     * Execute a method invoked by the client, checking parameters used.
761
     *
762 45
     * @param Request|string $req either a Request obj or a method name
763
     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
764
     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
765 45
     * @return Response
766 45
     *
767
     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
768
     */
769
    protected function execute($req, $params = null, $paramTypes = null)
770
    {
771
        static::$_xmlrpcs_occurred_errors = '';
772
        static::$_xmlrpc_debuginfo = '';
773
774
        if (is_object($req)) {
775 45
            $methodName = $req->method();
776 2
        } else {
777 2
            $methodName = $req;
778
        }
779 45
780
        $sysCall = $this->isSyscall($methodName);
781
        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
782 559
783
        if (!isset($dmap[$methodName]['function'])) {
784
            // No such method
785 559
            return new Response(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
786 64
        }
787
788 496
        // Check signature
789
        if (isset($dmap[$methodName]['signature'])) {
790
            $sig = $dmap[$methodName]['signature'];
791
            if (is_object($req)) {
792 559
                list($ok, $errStr) = $this->verifySignature($req, $sig);
793
            } else {
794
                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
795
            }
796
            if (!$ok) {
797
                // Didn't match.
798
                return new Response(
799
                    0,
800 559
                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
801
                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
802 559
                );
803 559
            }
804
        }
805
806
        $func = $dmap[$methodName]['function'];
807
808
        // let the 'class::function' syntax be accepted in dispatch maps
809 561
        if (is_string($func) && strpos($func, '::')) {
810
            $func = explode('::', $func);
811 561
        }
812 52
813
        // build string representation of function 'name'
814 509
        if (is_array($func)) {
815
            if (is_object($func[0])) {
816
                $funcName = get_class($func[0]) . '->' . $func[1];
817
            } else {
818
                $funcName = implode('::', $func);
819
            }
820
        } else if ($func instanceof \Closure) {
821
            $funcName = 'Closure';
822 559
        } else {
823
            $funcName = $func;
824 559
        }
825
826
        // verify that function to be invoked is in fact callable
827
        if (!is_callable($func)) {
828
            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
829
            return new Response(
830
                0,
831
                PhpXmlRpc::$xmlrpcerr['server_error'],
832
                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
833
            );
834
        }
835
836
        if (isset($dmap[$methodName]['exception_handling'])) {
837
            $exception_handling = (int)$dmap[$methodName]['exception_handling'];
838 127
        } else {
839
            $exception_handling = $this->exception_handling;
840 127
        }
841
842
        // If debug level is 3, we should catch all errors generated during processing of user function, and log them
843
        // as part of response
844
        if ($this->debug > 2) {
845
            self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
846 127
        }
847
848
        try {
849 127
            // Allow mixed-convention servers
850 127
            if (is_object($req)) {
851
                // call an 'xml-rpc aware' function
852
                if ($sysCall) {
853
                    $r = call_user_func($func, $this, $req);
854 127
                } else {
855 127
                    $r = call_user_func($func, $req);
856 127
                }
857
                if (!is_a($r, 'PhpXmlRpc\Response')) {
858
                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
859
                    if (is_a($r, 'PhpXmlRpc\Value')) {
860 127
                        $r = new Response($r);
861 127
                    } else {
862 127
                        $r = new Response(
863
                            0,
864
                            PhpXmlRpc::$xmlrpcerr['server_error'],
865
                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
866 127
                        );
867 127
                    }
868 127
                }
869
            } else {
870
                // call a 'plain php' function
871
                if ($sysCall) {
872 127
                    array_unshift($params, $this);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $array of array_unshift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

872
                    array_unshift(/** @scrutinizer ignore-type */ $params, $this);
Loading history...
873 127
                    $r = call_user_func_array($func, $params);
874 127
                } else {
875
                    // 3rd API convention for method-handling functions: EPI-style
876
                    if ($this->functions_parameters_type == 'epivals') {
877
                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
878
                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
879
                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
880
                            $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
881
                        } else {
882
                            // functions using EPI api should NOT return resp objects, so make sure we encode the
883
                            // return type correctly
884
                            $encoder = new Encoder();
885
                            $r = new Response($encoder->encode($r, array('extension_api')));
886
                        }
887
                    } else {
888
                        $r = call_user_func_array($func, $params);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $args of call_user_func_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

1142
    public static function _xmlrpcs_getCapabilities($server, /** @scrutinizer ignore-unused */ $req = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1143 85
    {
1144 64
        $encoder = new Encoder();
1145
        return new Response($encoder->encode($server->getCapabilities()));
1146
    }
1147
1148
    /**
1149
     * @internal handler of a system. method
1150
     *
1151
     * @param Server $server
1152
     * @param Request $req if called in plain php values mode, second param is missing
1153 85
     * @return Response
1154
     */
1155
    public static function _xmlrpcs_listMethods($server, $req = null)
0 ignored issues
show
Unused Code introduced by
The parameter $req is not used and could be removed. ( Ignorable by Annotation )

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

1155
    public static function _xmlrpcs_listMethods($server, /** @scrutinizer ignore-unused */ $req = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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