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

Request::getParam()   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 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Exception\HttpException;
6
use PhpXmlRpc\Helper\Http;
7
use PhpXmlRpc\Helper\XMLParser;
8
use PhpXmlRpc\Traits\CharsetEncoderAware;
9
use PhpXmlRpc\Traits\DeprecationLogger;
10
use PhpXmlRpc\Traits\ParserAware;
11
use PhpXmlRpc\Traits\PayloadBearer;
12
13
/**
14
 * This class provides the representation of a request to an XML-RPC server.
15
 * A client sends a PhpXmlrpc\Request to a server, and receives back an PhpXmlrpc\Response.
16
 *
17
 * @todo feature creep - add a protected $httpRequest member, in the same way the Response has one
18
 *
19
 * @property string $methodname deprecated - public access left in purely for BC. Access via method()/__construct()
20
 * @property Value[] $params deprecated - public access left in purely for BC. Access via getParam()/__construct()
21
 * @property int $debug deprecated - public access left in purely for BC. Access via .../setDebug()
22
 * @property string $payload deprecated - public access left in purely for BC. Access via getPayload()/setPayload()
23
 * @property string $content_type deprecated - public access left in purely for BC. Access via getContentType()/setPayload()
24
 */
25
class Request
26
{
27
    use CharsetEncoderAware;
28
    use DeprecationLogger;
29
    use ParserAware;
30
    use PayloadBearer;
31
32
    /** @var string */
33
    protected $methodname;
34 3
    /** @var Value[] */
35
    protected $params = array();
36 3
    /** @var int */
37 3
    protected $debug = 0;
38
39 3
    /**
40
     * holds data while parsing the response. NB: Not a full Response object
41
     * @deprecated will be removed in a future release
42
     */
43
    protected $httpResponse = array();
44
45
    /**
46
     * @param string $methodName the name of the method to invoke
47 707
     * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
48
     */
49 707
    public function __construct($methodName, $params = array())
50 8
    {
51
        $this->methodname = $methodName;
52 707
        foreach ($params as $param) {
53
            $this->addParam($param);
54
        }
55
    }
56
57
    /**
58
     * Gets/sets the xml-rpc method to be invoked.
59
     *
60 652
     * @param string $methodName the method to be set (leave empty not to set it)
61
     * @return string the method that will be invoked
62 652
     */
63 9
    public function method($methodName = '')
64
    {
65 652
        if ($methodName != '') {
66
            $this->methodname = $methodName;
67
        }
68
69
        return $this->methodname;
70
    }
71
72
    /**
73
     * Add a parameter to the list of parameters to be used upon method invocation.
74
     * Checks that $params is actually a Value object and not a plain php value.
75
     *
76
     * @param Value $param
77 715
     * @return boolean false on failure
78
     */
79 715
    public function addParam($param)
80 715
    {
81 500
        // check: do not add to self params which are not xml-rpc values
82
        if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
83 715
            $this->params[] = $param;
84
85
            return true;
86
        } else {
87
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': value passed in must be a PhpXmlRpc\Value');
88
            return false;
89
        }
90 652
    }
91
92 652
    /**
93 60
     * Returns the nth parameter in the request. The index zero-based.
94
     *
95 592
     * @param integer $i the index of the parameter to fetch (zero based)
96
     * @return Value the i-th parameter
97
     */
98
    public function getParam($i)
99
    {
100
        return $this->params[$i];
101
    }
102
103 652
    /**
104
     * Returns the number of parameters in the message.
105 652
     *
106
     * @return integer the number of parameters currently set
107
     */
108
    public function getNumParams()
109
    {
110
        return count($this->params);
111
    }
112 652
113
    /**
114 652
     * Returns xml representation of the message, XML prologue included. Sets `payload` and `content_type` properties
115 60
     *
116
     * @param string $charsetEncoding
117 592
     * @return string the xml representation of the message, xml prologue included
118
     */
119 652
    public function serialize($charsetEncoding = '')
120 652
    {
121 652
        $this->createPayload($charsetEncoding);
122 652
123 652
        return $this->payload;
124 609
    }
125 609
126
    /**
127 652
     * @internal this function will become protected in the future (and be folded into serialize)
128 652
     *
129 652
     * @param string $charsetEncoding
130
     * @return void
131
     */
132
    public function createPayload($charsetEncoding = '')
133
    {
134
        $this->logDeprecationUnlessCalledBy('serialize');
135
136
        if ($charsetEncoding != '') {
137
            $this->content_type = 'text/xml; charset=' . $charsetEncoding;
138 562
        } else {
139
            $this->content_type = 'text/xml';
140 562
        }
141
142
        $result = $this->xml_header($charsetEncoding);
143
        $result .= '<methodName>' . $this->getCharsetEncoder()->encodeEntities(
144 562
                $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
145
        $result .= "<params>\n";
146
        foreach ($this->params as $p) {
147
            $result .= "<param>\n" . $p->serialize($charsetEncoding) .
148
                "</param>\n";
149
        }
150
        $result .= "</params>\n";
151
        $result .= $this->xml_footer();
152
153
        $this->payload = $result;
154 652
    }
155
156 652
    /**
157
     * @internal this function will become protected in the future (and be folded into serialize)
158 652
     *
159
     * @param string $charsetEncoding
160
     * @return string
161
     */
162
    public function xml_header($charsetEncoding = '')
163
    {
164
        $this->logDeprecationUnlessCalledBy('createPayload');
165
166
        if ($charsetEncoding != '') {
167
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
168
        } else {
169
            return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
170 657
        }
171
    }
172
173 657
    /**
174 657
     * @internal this function will become protected in the future (and be folded into serialize)
175
     *
176 657
     * @return string
177
     */
178
    public function xml_footer()
179
    {
180
        $this->logDeprecationUnlessCalledBy('createPayload');
181
182
        return '</methodCall>';
183
    }
184
185
    /**
186
     * Given an open file handle, read all data available and parse it as an xml-rpc response.
187
     *
188
     * NB: the file handle is not closed by this function.
189 517
     * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
190
     *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
191 517
     *      because we cannot trust the caller to give us a valid pointer to an open file...
192
     *
193
     * @param resource $fp stream pointer
194
     * @param bool $headersProcessed
195
     * @param string $returnType
196
     * @return Response
197
     *
198
     * @todo arsing Responses is not really the responsibility of the Request class. Maybe of the Client...
199 538
     * @todo feature creep - add a flag to disable trying to parse the http headers
200
     */
201 538
    public function parseResponseFile($fp, $headersProcessed = false, $returnType = 'xmlrpcvals')
202
    {
203
        $ipd = '';
204
        // q: is there an optimal buffer size? Is there any value in making the buffer size a tuneable?
205
        while ($data = fread($fp, 32768)) {
206
            $ipd .= $data;
207
        }
208
        return $this->parseResponse($ipd, $headersProcessed, $returnType);
209
    }
210
211
    /**
212
     * Parse the xml-rpc response contained in the string $data and return a Response object.
213
     *
214
     * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
215
     *
216
     * @param string $data the xml-rpc response, possibly including http headers
217
     * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
218
     *                               consequent decoding
219
     * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
220
     *                           'phpvals'
221
     * @return Response
222
     *
223
     * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client...
224
     * @todo what about only populating 'raw_data' in httpResponse when debug mode is > 0?
225
     * @todo feature creep - allow parsing data gotten from a stream pointer instead of a string: read it piecewise,
226
     *       looking first for separation between headers and body, then for charset indicators, server debug info and
227
     *       </methodResponse>. That would require a notable increase in code complexity...
228
     */
229
    public function parseResponse($data = '', $headersProcessed = false, $returnType = XMLParser::RETURN_XMLRPCVALS)
230
    {
231
        if ($this->debug > 0) {
232
            $this->getLogger()->debug("---GOT---\n$data\n---END---");
233
        }
234
235
        $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
0 ignored issues
show
Deprecated Code introduced by
The property PhpXmlRpc\Request::$httpResponse has been deprecated: will be removed in a future release ( Ignorable by Annotation )

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

235
        /** @scrutinizer ignore-deprecated */ $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());

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...
236
237
        if ($data == '') {
238
            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': no response received from server.');
239
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
240
        }
241
242 711
        // parse the HTTP headers of the response, if present, and separate them from data
243
        if (substr($data, 0, 4) == 'HTTP') {
244 711
            $httpParser = new Http();
245 3
            try {
246
                $httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug > 0);
0 ignored issues
show
Bug introduced by
$this->debug > 0 of type boolean is incompatible with the type integer expected by parameter $debug of PhpXmlRpc\Helper\Http::parseResponseHeaders(). ( Ignorable by Annotation )

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

246
                $httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, /** @scrutinizer ignore-type */ $this->debug > 0);
Loading history...
247
            } catch (HttpException $e) {
248 711
                // failed processing of HTTP response headers
249
                // save into response obj the full payload received, for debugging
250 711
                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode()));
251
            } catch(\Exception $e) {
252
                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data));
253
            }
254
        } else {
255
            $httpResponse = $this->httpResponse;
0 ignored issues
show
Deprecated Code introduced by
The property PhpXmlRpc\Request::$httpResponse has been deprecated: will be removed in a future release ( Ignorable by Annotation )

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

255
            $httpResponse = /** @scrutinizer ignore-deprecated */ $this->httpResponse;

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...
256 711
        }
257 699
258
        // be tolerant of extra whitespace in response body
259 699
        $data = trim($data);
260 3
261
        /// @todo optimization creep - return an error msg if $data == ''
262
263 3
        // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
264
        // idea from Luca Mariano <[email protected]> originally in PEARified version of the lib
265
        $pos = strrpos($data, '</methodResponse>');
266
        if ($pos !== false) {
267
            $data = substr($data, 0, $pos + 17);
268
        }
269
270 708
        // try to 'guestimate' the character encoding of the received response
271
        $respEncoding = XMLParser::guessEncoding(
272
            isset($httpResponse['headers']['content-type']) ? $httpResponse['headers']['content-type'] : '',
273
            $data
274
        );
275
276 708
        if ($this->debug >= 0) {
277 708
            $this->httpResponse = $httpResponse;
0 ignored issues
show
Deprecated Code introduced by
The property PhpXmlRpc\Request::$httpResponse has been deprecated: will be removed in a future release ( Ignorable by Annotation )

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

277
            /** @scrutinizer ignore-deprecated */ $this->httpResponse = $httpResponse;

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...
278 708
        } else {
279
            $httpResponse = null;
280
        }
281
282 708
        if ($this->debug > 0) {
283
            $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
284 708
            if ($start) {
285 3
                $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
286 3
                /// @todo what if there is no end tag?
287 3
                $end = strpos($data, '-->', $start);
288 3
                $comments = substr($data, $start, $end - $start);
289 3
                $this->getLogger()->debug("---SERVER DEBUG INFO (DECODED)---\n\t" .
290 3
                    str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", array('encoding' => $respEncoding));
291 3
            }
292
        }
293
294
        // if the user wants back raw xml, give it to her
295
        if ($returnType == 'xml') {
296 708
            return new Response($data, 0, '', 'xml', $httpResponse);
297 1
        }
298
299
        /// @todo move this block of code into the XMLParser
300 707
        if ($respEncoding != '') {
301
            // Since parsing will fail if charset is not specified in the xml declaration,
302
            // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
303
            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
304
            //if (!is_valid_charset($respEncoding, array('UTF-8')))
305
            if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
306 707
                if (function_exists('mb_convert_encoding')) {
307 1
                    $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
308 1
                } else {
309
                    if ($respEncoding == 'ISO-8859-1') {
310
                        $data = utf8_encode($data);
311
                    } else {
312
                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received response: ' . $respEncoding);
313
                    }
314
                }
315
            }
316
        }
317
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
318
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
319
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
320
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
321
        } else {
322
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
323
        }
324 707
325
        $xmlRpcParser = $this->getParser();
326
        $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
327
        $_xh = $xmlRpcParser->_xh;
328 707
329
        // first error check: xml not well-formed
330
        if ($_xh['isf'] == 3) {
331 707
332 707
            // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
333
334
            // Q: should we give back an error with variable error number, as we do server-side? But if we do, will
335 707
            //    we be able to tell apart the two cases? In theory, we never emit invalid xml on our end, but
336
            //    there could be proxies meddling with the request, or network data corruption...
337
338
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_xml'],
339 3
                PhpXmlRpc::$xmlrpcstr['invalid_xml'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
340 3
341 3
            if ($this->debug > 0) {
342
                $this->getLogger()->debug($_xh['isf_reason']);
343
            }
344 3
        }
345 3
        // second error check: xml well-formed but not xml-rpc compliant
346
        elseif ($_xh['isf'] == 2) {
347
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_not_compliant'],
348
                PhpXmlRpc::$xmlrpcstr['xml_not_compliant'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
349 707
350 4
            /// @todo echo something for the user? check if it was already done by the parser...
351 4
            //if ($this->debug > 0) {
352 4
            //    $this->getLogger()->debug($_xh['isf_reason']);
353
            //}
354
        }
355 4
        // third error check: parsing of the response has somehow gone boink.
356
        /// @todo shall we omit this check, since we trust the parsing code?
357
        elseif ($_xh['isf'] > 3 || $returnType == XMLParser::RETURN_XMLRPCVALS && !is_object($_xh['value'])) {
358
            // something odd has happened and it's time to generate a client side error indicating something odd went on
359
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_parsing_error'], PhpXmlRpc::$xmlrpcstr['xml_parsing_error'],
360
                '', $httpResponse
361 704
            );
362
363
            /// @todo echo something for the user?
364
        } else {
365
            if ($this->debug > 1) {
366
                $this->getLogger()->debug(
367
                    "---PARSED---\n".var_export($_xh['value'], true)."\n---END---"
368
                );
369 704
            }
370 2
371 2
            $v = $_xh['value'];
372
373
            if ($_xh['isf']) {
374
                /// @todo we should test (here or preferably in the parser) if server sent an int and a string, and/or
375 704
                ///       coerce them into such...
376
                if ($returnType == XMLParser::RETURN_XMLRPCVALS) {
377 704
                    $errNo_v = $v['faultCode'];
378
                    $errStr_v = $v['faultString'];
379 109
                    $errNo = $errNo_v->scalarVal();
380 109
                    $errStr = $errStr_v->scalarVal();
381 109
                } else {
382 109
                    $errNo = $v['faultCode'];
383 109
                    $errStr = $v['faultString'];
384
                }
385
386
                if ($errNo == 0) {
387
                    // FAULT returned, errno needs to reflect that
388
                    /// @todo feature creep - add this code to PhpXmlRpc::$xmlrpcerr
389 109
                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': fault response received with faultCode 0 or null. Converted it to -1');
390
                    $errNo = -1;
391
                }
392
393
                $r = new Response(0, $errNo, $errStr, '', $httpResponse);
394 109
            } else {
395
                $r = new Response($v, 0, '', $returnType, $httpResponse);
396 680
            }
397
        }
398
399
        return $r;
400 707
    }
401
402
    /**
403
     * Kept the old name even if Request class was renamed, for BC.
404
     *
405
     * @return string
406
     */
407
    public function kindOf()
408 150
    {
409
        return 'msg';
410 150
    }
411
412
    /**
413
     * Enables/disables the echoing to screen of the xml-rpc responses received.
414
     *
415
     * @param integer $level values <0, 0, 1, >1 are supported
416
     * @return $this
417
     */
418 712
    public function setDebug($level)
419
    {
420 712
        $this->debug = $level;
421 712
        return $this;
422
    }
423
424
    // *** BC layer ***
425
426
    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
427
    public function &__get($name)
428
    {
429
        switch ($name) {
430
            case 'me':
431
            case 'mytype':
432
            case '_php_class':
433
            case 'payload':
434
            case 'content_type':
435
                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
436
                return $this->$name;
437
            default:
438
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
439
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
440
                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
441
                $result = null;
442
                return $result;
443
        }
444
    }
445
446
    public function __set($name, $value)
447
    {
448
        switch ($name) {
449
            case 'methodname':
450
            case 'params':
451
            case 'debug':
452
            case 'payload':
453
            case 'content_type':
454
                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
455
                $this->$name = $value;
456
                break;
457
            default:
458
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
459
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
460
                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
461
        }
462
    }
463
464
    public function __isset($name)
465
    {
466
        switch ($name) {
467
            case 'methodname':
468
            case 'params':
469
            case 'debug':
470
            case 'payload':
471
            case 'content_type':
472
                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
473
                return isset($this->$name);
474
            default:
475
                return false;
476
        }
477
    }
478
479
    public function __unset($name)
480
    {
481
        switch ($name) {
482
            case 'methodname':
483
            case 'params':
484
            case 'debug':
485
            case 'payload':
486
            case 'content_type':
487
                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
488
                unset($this->$name);
489
                break;
490
            default:
491
                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
492
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
493
                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
494
        }
495
    }
496
}
497