Passed
Push — master ( b6cd05...9f5262 )
by Gaetano
05:39
created

Request::addParam()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

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

214
        /** @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...
215
216
        if ($data == '') {
217
            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': no response received from server.');
218
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
219
        }
220
221
        // parse the HTTP headers of the response, if present, and separate them from data
222
        if (substr($data, 0, 4) == 'HTTP') {
223
            $httpParser = new Http();
224
            try {
225
                $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

225
                $httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, /** @scrutinizer ignore-type */ $this->debug > 0);
Loading history...
226
            } catch (HttpException $e) {
227
                // failed processing of HTTP response headers
228
                // save into response obj the full payload received, for debugging
229
                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode()));
230
            } catch(\Exception $e) {
231
                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data));
232
            }
233
        }
234
235
        // be tolerant of extra whitespace in response body
236
        $data = trim($data);
237
238
        /// @todo optimization creep - return an error msg if $data == ''
239
240
        // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
241
        // idea from Luca Mariano <[email protected]> originally in PEARified version of the lib
242 711
        $pos = strrpos($data, '</methodResponse>');
243
        if ($pos !== false) {
244 711
            $data = substr($data, 0, $pos + 17);
245 3
        }
246
247
        // try to 'guestimate' the character encoding of the received response
248 711
        $respEncoding = XMLParser::guessEncoding(
249
            isset($httpResponse['headers']['content-type']) ? $httpResponse['headers']['content-type'] : '',
250 711
            $data
251
        );
252
253
        if ($this->debug >= 0) {
254
            $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

254
            /** @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...
255
        } else {
256 711
            $httpResponse = null;
257 699
        }
258
259 699
        if ($this->debug > 0) {
260 3
            $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
261
            if ($start) {
262
                $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
263 3
                $end = strpos($data, '-->', $start);
264
                $comments = substr($data, $start, $end - $start);
265
                $this->getLogger()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
266
                    str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding);
267
            }
268
        }
269
270 708
        // if the user wants back raw xml, give it to her
271
        if ($returnType == 'xml') {
272
            return new Response($data, 0, '', 'xml', $httpResponse);
273
        }
274
275
        /// @todo move this block of code into the XMLParser
276 708
        if ($respEncoding != '') {
277 708
            // Since parsing will fail if charset is not specified in the xml declaration,
278 708
            // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
279
            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
280
            //if (!is_valid_charset($respEncoding, array('UTF-8')))
281
            if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
282 708
                if (function_exists('mb_convert_encoding')) {
283
                    $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
284 708
                } else {
285 3
                    if ($respEncoding == 'ISO-8859-1') {
286 3
                        $data = utf8_encode($data);
287 3
                    } else {
288 3
                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received response: ' . $respEncoding);
289 3
                    }
290 3
                }
291 3
            }
292
        }
293
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
294
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
295
        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
296 708
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
297 1
        } else {
298
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
299
        }
300 707
301
        $xmlRpcParser = $this->getParser();
302
        $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
303
304
        // first error check: xml not well-formed
305
        if ($xmlRpcParser->_xh['isf'] == 3) {
306 707
307 1
            // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
308 1
309
            // Q: should we give back an error with variable error number, as we do server-side? But if we do, will
310
            //    we be able to tell apart the two cases? In theory, we never emit invalid xml on our end, but
311
            //    there could be proxies meddling with the request, or network data corruption...
312
313
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_xml'],
314
                PhpXmlRpc::$xmlrpcstr['invalid_xml'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '',
315
                $httpResponse
316
            );
317
318
            if ($this->debug > 0) {
319
                $this->getLogger()->debugMessage($xmlRpcParser->_xh['isf_reason']);
320
            }
321
        }
322
        // second error check: xml well-formed but not xml-rpc compliant
323
        elseif ($xmlRpcParser->_xh['isf'] == 2) {
324 707
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_not_compliant'],
325
                PhpXmlRpc::$xmlrpcstr['xml_not_compliant'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '',
326
                $httpResponse
327
            );
328 707
329
            /// @todo echo something for the user? check if this was already done by the parser...
330
            //if ($this->debug > 0) {
331 707
            //    $this->getLogger()->debugMessage($xmlRpcParser->_xh['isf_reason']);
332 707
            //}
333
        }
334
        // third error check: parsing of the response has somehow gone boink.
335 707
        /// @todo shall we omit this check, since we trust the parsing code?
336
        elseif ($xmlRpcParser->_xh['isf'] > 3 || $returnType == XMLParser::RETURN_XMLRPCVALS && !is_object($xmlRpcParser->_xh['value'])) {
337
            // something odd has happened and it's time to generate a client side error indicating something odd went on
338
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_parsing_error'], PhpXmlRpc::$xmlrpcstr['xml_parsing_error'],
339 3
                '', $httpResponse
340 3
            );
341 3
342
            /// @todo echo something for the user?
343
        } else {
344 3
            if ($this->debug > 1) {
345 3
                $this->getLogger()->debugMessage(
346
                    "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
347
                );
348
            }
349 707
350 4
            $v = $xmlRpcParser->_xh['value'];
351 4
352 4
            if ($xmlRpcParser->_xh['isf']) {
353
                /// @todo we should test here if server sent an int and a string, and/or coerce them into such...
354
                if ($returnType == XMLParser::RETURN_XMLRPCVALS) {
355 4
                    $errNo_v = $v['faultCode'];
356
                    $errStr_v = $v['faultString'];
357
                    $errNo = $errNo_v->scalarval();
358
                    $errStr = $errStr_v->scalarval();
359
                } else {
360
                    $errNo = $v['faultCode'];
361 704
                    $errStr = $v['faultString'];
362
                }
363
364
                if ($errNo == 0) {
365
                    // FAULT returned, errno needs to reflect that
366
                    /// @todo we should signal somehow that the server returned a fault with code 0?
367
                    $errNo = -1;
368
                }
369 704
370 2
                $r = new Response(0, $errNo, $errStr, '', $httpResponse);
371 2
            } else {
372
                $r = new Response($v, 0, '', $returnType, $httpResponse);
373
            }
374
        }
375 704
376
        return $r;
377 704
    }
378
379 109
    /**
380 109
     * Kept the old name even if Request class was renamed, for BC.
381 109
     *
382 109
     * @return string
383 109
     */
384
    public function kindOf()
385
    {
386
        return 'msg';
387
    }
388
389 109
    /**
390
     * Enables/disables the echoing to screen of the xml-rpc responses received.
391
     *
392
     * @param integer $level values <0, 0, 1, >1 are supported
393
     * @return $this
394 109
     */
395
    public function setDebug($level)
396 680
    {
397
        $this->debug = $level;
398
        return $this;
399
    }
400
}
401