Passed
Push — master ( da2c1e...110169 )
by Gaetano
04:55 queued 01:13
created

Request   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Test Coverage

Coverage 85.83%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 129
c 7
b 0
f 0
dl 0
loc 365
ccs 109
cts 127
cp 0.8583
rs 8.8
wmc 45

13 Methods

Rating   Name   Duplication   Size   Complexity  
A xml_footer() 0 3 1
A __construct() 0 5 2
A addParam() 0 9 3
A getNumParams() 0 3 1
A getParam() 0 3 1
A xml_header() 0 6 2
A kindOf() 0 3 1
F parseResponse() 0 162 25
A parseResponseFile() 0 7 2
A method() 0 7 2
A setDebug() 0 3 1
A serialize() 0 5 1
A createPayload() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like Request often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Request, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpXmlRpc;
4
5
use PhpXmlRpc\Helper\Charset;
6
use PhpXmlRpc\Helper\Http;
7
use PhpXmlRpc\Helper\Logger;
8
use PhpXmlRpc\Helper\XMLParser;
9
10
/**
11
 * This class provides the representation of a request to an XML-RPC server.
12
 * A client sends a PhpXmlrpc\Request to a server, and receives back an PhpXmlrpc\Response.
13
 */
14
class Request
15
{
16
    /// @todo: do these need to be public?
17
    public $payload;
18
    /** @internal */
19
    public $methodname;
20
    /** @internal */
21
    public $params = array();
22
    public $debug = 0;
23
    public $content_type = 'text/xml';
24
25
    // holds data while parsing the response. NB: Not a full Response object
26
    protected $httpResponse = array();
27
28
    /**
29
     * @param string $methodName the name of the method to invoke
30
     * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
31
     */
32 609
    public function __construct($methodName, $params = array())
33
    {
34 609
        $this->methodname = $methodName;
35 609
        foreach ($params as $param) {
36 434
            $this->addParam($param);
37
        }
38 609
    }
39
40
    /**
41
     * @internal this function will become protected in the future
42
     * @param string $charsetEncoding
43
     * @return string
44
     */
45 550
    public function xml_header($charsetEncoding = '')
46
    {
47 550
        if ($charsetEncoding != '') {
48 56
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
49
        } else {
50 494
            return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
51
        }
52
    }
53
54
    /**
55
     * @internal this function will become protected in the future
56
     * @return string
57
     */
58 550
    public function xml_footer()
59
    {
60 550
        return '</methodCall>';
61
    }
62
63
    /**
64
     * @internal this function will become protected in the future
65
     * @param string $charsetEncoding
66
     */
67 550
    public function createPayload($charsetEncoding = '')
68
    {
69 550
        if ($charsetEncoding != '') {
70 56
            $this->content_type = 'text/xml; charset=' . $charsetEncoding;
71
        } else {
72 494
            $this->content_type = 'text/xml';
73
        }
74 550
        $this->payload = $this->xml_header($charsetEncoding);
75 550
        $this->payload .= '<methodName>' . Charset::instance()->encodeEntities(
76 550
            $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
77 550
        $this->payload .= "<params>\n";
78 550
        foreach ($this->params as $p) {
79 511
            $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) .
80 506
                "</param>\n";
81
        }
82 550
        $this->payload .= "</params>\n";
83 550
        $this->payload .= $this->xml_footer();
84 550
    }
85
86
    /**
87
     * Gets/sets the xmlrpc method to be invoked.
88
     *
89
     * @param string $methodName the method to be set (leave empty not to set it)
90
     *
91
     * @return string the method that will be invoked
92
     */
93 489
    public function method($methodName = '')
94
    {
95 489
        if ($methodName != '') {
96
            $this->methodname = $methodName;
97
        }
98
99 489
        return $this->methodname;
100
    }
101
102
    /**
103
     * Returns xml representation of the message. XML prologue included.
104
     *
105
     * @param string $charsetEncoding
106
     *
107
     * @return string the xml representation of the message, xml prologue included
108
     */
109 3
    public function serialize($charsetEncoding = '')
110
    {
111 3
        $this->createPayload($charsetEncoding);
112
113 3
        return $this->payload;
114
    }
115
116
    /**
117
     * Add a parameter to the list of parameters to be used upon method invocation.
118
     *
119
     * Checks that $params is actually a Value object and not a plain php value.
120
     *
121
     * @param Value $param
122
     *
123
     * @return boolean false on failure
124
     */
125 555
    public function addParam($param)
126
    {
127
        // check: do not add to self params which are not xmlrpc values
128 555
        if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
129 555
            $this->params[] = $param;
130
131 555
            return true;
132
        } else {
133
            return false;
134
        }
135
    }
136
137
    /**
138
     * Returns the nth parameter in the request. The index zero-based.
139
     *
140
     * @param integer $i the index of the parameter to fetch (zero based)
141
     *
142
     * @return Value the i-th parameter
143
     */
144 449
    public function getParam($i)
145
    {
146 449
        return $this->params[$i];
147
    }
148
149
    /**
150
     * Returns the number of parameters in the message.
151
     *
152
     * @return integer the number of parameters currently set
153
     */
154 468
    public function getNumParams()
155
    {
156 468
        return count($this->params);
157
    }
158
159
    /**
160
     * Given an open file handle, read all data available and parse it as an xmlrpc response.
161
     *
162
     * NB: the file handle is not closed by this function.
163
     * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
164
     *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
165
     *      because we cannot trust the caller to give us a valid pointer to an open file...
166
     *
167
     * @param resource $fp stream pointer
168
     * @param bool $headersProcessed
169
     * @param string $returnType
170
     *
171
     * @return Response
172
     */
173
    public function parseResponseFile($fp, $headersProcessed = false, $returnType = 'xmlrpcvals')
174
    {
175
        $ipd = '';
176
        while ($data = fread($fp, 32768)) {
177
            $ipd .= $data;
178
        }
179
        return $this->parseResponse($ipd, $headersProcessed, $returnType);
180
    }
181
182
    /**
183
     * Parse the xmlrpc response contained in the string $data and return a Response object.
184
     *
185
     * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
186
     *
187
     * @param string $data the xmlrpc response, possibly including http headers
188
     * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
189
     *                               consequent decoding
190
     * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
191
     *                           'phpvals'
192
     *
193
     * @return Response
194
     *
195
     * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client...
196
     */
197 606
    public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals')
198
    {
199 606
        if ($this->debug) {
200 3
            Logger::instance()->debugMessage("---GOT---\n$data\n---END---");
201
        }
202
203 606
        $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
204
205 606
        if ($data == '') {
206
            Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': no response received from server.');
207
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
208
        }
209
210
        // parse the HTTP headers of the response, if present, and separate them from data
211 606
        if (substr($data, 0, 4) == 'HTTP') {
212 594
            $httpParser = new Http();
213
            try {
214 594
                $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug);
215 1
            } catch(\Exception $e) {
216 1
                $r = new Response(0, $e->getCode(), $e->getMessage());
217
                // failed processing of HTTP response headers
218
                // save into response obj the full payload received, for debugging
219 1
                $r->raw_data = $data;
220
221 1
                return $r;
222
            }
223
        }
224
225
        // be tolerant of extra whitespace in response body
226 605
        $data = trim($data);
227
228
        /// @todo return an error msg if $data == '' ?
229
230
        // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
231
        // idea from Luca Mariano <[email protected]> originally in PEARified version of the lib
232 605
        $pos = strrpos($data, '</methodResponse>');
233 605
        if ($pos !== false) {
234 605
            $data = substr($data, 0, $pos + 17);
235
        }
236
237
        // try to 'guestimate' the character encoding of the received response
238 605
        $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data);
239
240 605
        if ($this->debug) {
241 3
            $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
242 3
            if ($start) {
243 3
                $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
244 3
                $end = strpos($data, '-->', $start);
245 3
                $comments = substr($data, $start, $end - $start);
246 3
                Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
247 3
                    str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding);
248
            }
249
        }
250
251
        // if user wants back raw xml, give it to her
252 605
        if ($returnType == 'xml') {
253 1
            $r = new Response($data, 0, '', 'xml');
254 1
            $r->hdrs = $this->httpResponse['headers'];
255 1
            $r->_cookies = $this->httpResponse['cookies'];
256 1
            $r->raw_data = $this->httpResponse['raw_data'];
257
258 1
            return $r;
259
        }
260
261 604
        if ($respEncoding != '') {
262
263
            // Since parsing will fail if charset is not specified in the xml prologue,
264
            // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
265
            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
266
            //if (!is_valid_charset($respEncoding, array('UTF-8')))
267 604
            if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
268 1
                if ($respEncoding == 'ISO-8859-1') {
269 1
                    $data = utf8_encode($data);
270
                } else {
271
                    if (extension_loaded('mbstring')) {
272
                        $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
273
                    } else {
274
                        Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding);
275
                    }
276
                }
277
            }
278
        }
279
280
        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
281
        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, ie. utf8
282
        // This allows to send data which is native in various charset, by extending xmlrpc_encode_entities() and
283
        // setting xmlrpc_internalencoding
284 604
        if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
285
            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8');
286
        } else {
287 604
            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
288
        }
289
290 604
        $xmlRpcParser = new XMLParser($options);
291 604
        $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE);
292
293
        // first error check: xml not well formed
294 604
        if ($xmlRpcParser->_xh['isf'] > 2) {
295
296
            // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
297
298 3
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
299 3
                PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
300
301 3
            if ($this->debug) {
302 3
                print $xmlRpcParser->_xh['isf_reason'];
303
            }
304
        }
305
        // second error check: xml well formed but not xml-rpc compliant
306 604
        elseif ($xmlRpcParser->_xh['isf'] == 2) {
307 4
            if ($this->debug) {
308
                /// @todo echo something for user?
309
            }
310
311 4
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
312 4
                PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
313
        }
314
        // third error check: parsing of the response has somehow gone boink.
315
        // NB: shall we omit this check, since we trust the parsing code?
316 601
        elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) {
317
            // something odd has happened
318
            // and it's time to generate a client side error
319
            // indicating something odd went on
320
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
321
                PhpXmlRpc::$xmlrpcstr['invalid_return']);
322
        } else {
323 601
            if ($this->debug > 1) {
324 2
                Logger::instance()->debugMessage(
325 2
                    "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
326
                );
327
            }
328
329 601
            $v = $xmlRpcParser->_xh['value'];
330
331 601
            if ($xmlRpcParser->_xh['isf']) {
332
                /// @todo we should test here if server sent an int and a string, and/or coerce them into such...
333 80
                if ($returnType == 'xmlrpcvals') {
334 80
                    $errNo_v = $v['faultCode'];
335 80
                    $errStr_v = $v['faultString'];
336 80
                    $errNo = $errNo_v->scalarval();
337 80
                    $errStr = $errStr_v->scalarval();
338
                } else {
339
                    $errNo = $v['faultCode'];
340
                    $errStr = $v['faultString'];
341
                }
342
343 80
                if ($errNo == 0) {
344
                    // FAULT returned, errno needs to reflect that
345
                    $errNo = -1;
346
                }
347
348 80
                $r = new Response(0, $errNo, $errStr);
349
            } else {
350 598
                $r = new Response($v, 0, '', $returnType);
351
            }
352
        }
353
354 604
        $r->hdrs = $this->httpResponse['headers'];
355 604
        $r->_cookies = $this->httpResponse['cookies'];
356 604
        $r->raw_data = $this->httpResponse['raw_data'];
357
358 604
        return $r;
359
    }
360
361
    /**
362
     * Kept the old name even if Request class was renamed, for compatibility.
363
     *
364
     * @return string
365
     */
366 117
    public function kindOf()
367
    {
368 117
        return 'msg';
369
    }
370
371
    /**
372
     * Enables/disables the echoing to screen of the xmlrpc responses received.
373
     *
374
     * @param integer $level values 0, 1, 2 are supported
375
     */
376 607
    public function setDebug($level)
377
    {
378 607
        $this->debug = $level;
379 607
    }
380
}
381