Passed
Push — master ( 526b0d...b337d2 )
by Gaetano
04:53
created

Request::method()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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