Completed
Push — master ( 5fc0c8...b5d242 )
by Gaetano
06:30
created

Request::serialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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
    public $methodname;
19
    public $params = array();
20
    public $debug = 0;
21
    public $content_type = 'text/xml';
22
23
    // holds data while parsing the response. NB: Not a full Response object
24
    protected $httpResponse = array();
25
26
    /**
27
     * @param string $methodName the name of the method to invoke
28
     * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
29
     */
30
    public function __construct($methodName, $params = array())
31
    {
32
        $this->methodname = $methodName;
33
        foreach ($params as $param) {
34
            $this->addParam($param);
35
        }
36
    }
37
38
    public function xml_header($charsetEncoding = '')
39
    {
40
        if ($charsetEncoding != '') {
41
            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
42
        } else {
43
            return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
44
        }
45
    }
46
47
    public function xml_footer()
48
    {
49
        return '</methodCall>';
50
    }
51
52
    public function createPayload($charsetEncoding = '')
53
    {
54
        if ($charsetEncoding != '') {
55
            $this->content_type = 'text/xml; charset=' . $charsetEncoding;
56
        } else {
57
            $this->content_type = 'text/xml';
58
        }
59
        $this->payload = $this->xml_header($charsetEncoding);
60
        $this->payload .= '<methodName>' . Charset::instance()->encodeEntities(
61
            $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
62
        $this->payload .= "<params>\n";
63
        foreach ($this->params as $p) {
64
            $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) .
65
                "</param>\n";
66
        }
67
        $this->payload .= "</params>\n";
68
        $this->payload .= $this->xml_footer();
69
    }
70
71
    /**
72
     * Gets/sets the xmlrpc method to be invoked.
73
     *
74
     * @param string $methodName the method to be set (leave empty not to set it)
75
     *
76
     * @return string the method that will be invoked
77
     */
78
    public function method($methodName = '')
79
    {
80
        if ($methodName != '') {
81
            $this->methodname = $methodName;
82
        }
83
84
        return $this->methodname;
85
    }
86
87
    /**
88
     * Returns xml representation of the message. XML prologue included.
89
     *
90
     * @param string $charsetEncoding
91
     *
92
     * @return string the xml representation of the message, xml prologue included
93
     */
94
    public function serialize($charsetEncoding = '')
95
    {
96
        $this->createPayload($charsetEncoding);
97
98
        return $this->payload;
99
    }
100
101
    /**
102
     * Add a parameter to the list of parameters to be used upon method invocation.
103
     *
104
     * Checks that $params is actually a Value object and not a plain php value.
105
     *
106
     * @param Value $param
107
     *
108
     * @return boolean false on failure
109
     */
110
    public function addParam($param)
111
    {
112
        // add check: do not add to self params which are not xmlrpc values
113
        if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
114
            $this->params[] = $param;
115
116
            return true;
117
        } else {
118
            return false;
119
        }
120
    }
121
122
    /**
123
     * Returns the nth parameter in the request. The index zero-based.
124
     *
125
     * @param integer $i the index of the parameter to fetch (zero based)
126
     *
127
     * @return Value the i-th parameter
128
     */
129
    public function getParam($i)
130
    {
131
        return $this->params[$i];
132
    }
133
134
    /**
135
     * Returns the number of parameters in the message.
136
     *
137
     * @return integer the number of parameters currently set
138
     */
139
    public function getNumParams()
140
    {
141
        return count($this->params);
142
    }
143
144
    /**
145
     * Given an open file handle, read all data available and parse it as an xmlrpc response.
146
     *
147
     * NB: the file handle is not closed by this function.
148
     * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
149
     *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
150
     *      because we cannot trust the caller to give us a valid pointer to an open file...
151
     *
152
     * @param resource $fp stream pointer
153
     *
154
     * @return Response
155
     *
156
     * @todo add 2nd & 3rd param to be passed to ParseResponse() ???
157
     */
158
    public function parseResponseFile($fp)
159
    {
160
        $ipd = '';
161
        while ($data = fread($fp, 32768)) {
162
            $ipd .= $data;
163
        }
164
        return $this->parseResponse($ipd);
165
    }
166
167
    /**
168
     * Parse the xmlrpc response contained in the string $data and return a Response object.
169
     *
170
     * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
171
     *
172
     * @param string $data the xmlrpc response, possibly including http headers
173
     * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
174
     *                               consequent decoding
175
     * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
176
     *                           'phpvals'
177
     *
178
     * @return Response
179
     */
180
    public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals')
181
    {
182
        if ($this->debug) {
183
            Logger::instance()->debugMessage("---GOT---\n$data\n---END---");
184
        }
185
186
        $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
187
188 View Code Duplication
        if ($data == '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
189
            error_log('XML-RPC: ' . __METHOD__ . ': no response received from server.');
190
            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
191
        }
192
193
        // parse the HTTP headers of the response, if present, and separate them from data
194
        if (substr($data, 0, 4) == 'HTTP') {
195
            $httpParser = new Http();
196
            try {
197
                $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug);
198
            } catch(\Exception $e) {
199
                $r = new Response(0, $e->getCode(), $e->getMessage());
200
                // failed processing of HTTP response headers
201
                // save into response obj the full payload received, for debugging
202
                $r->raw_data = $data;
203
204
                return $r;
205
            }
206
        }
207
208
        // be tolerant of extra whitespace in response body
209
        $data = trim($data);
210
211
        /// @todo return an error msg if $data=='' ?
212
213
        // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
214
        // idea from Luca Mariano <[email protected]> originally in PEARified version of the lib
215
        $pos = strrpos($data, '</methodResponse>');
216
        if ($pos !== false) {
217
            $data = substr($data, 0, $pos + 17);
218
        }
219
220
        // try to 'guestimate' the character encoding of the received response
221
        $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data);
222
223
        if ($this->debug) {
224
            $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
225
            if ($start) {
226
                $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
227
                $end = strpos($data, '-->', $start);
228
                $comments = substr($data, $start, $end - $start);
229
                Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
230
                    str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding);
231
            }
232
        }
233
234
        // if user wants back raw xml, give it to him
235
        if ($returnType == 'xml') {
236
            $r = new Response($data, 0, '', 'xml');
237
            $r->hdrs = $this->httpResponse['headers'];
238
            $r->_cookies = $this->httpResponse['cookies'];
239
            $r->raw_data = $this->httpResponse['raw_data'];
240
241
            return $r;
242
        }
243
244 View Code Duplication
        if ($respEncoding != '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
245
246
            // Since parsing will fail if charset is not specified in the xml prologue,
247
            // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
248
            // The following code might be better for mb_string enabled installs, but
249
            // makes the lib about 200% slower...
250
            //if (!is_valid_charset($respEncoding, array('UTF-8')))
0 ignored issues
show
Unused Code Comprehensibility introduced by
80% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
251
            if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
252
                if ($respEncoding == 'ISO-8859-1') {
253
                    $data = utf8_encode($data);
254
                } else {
255
                    if (extension_loaded('mbstring')) {
256
                        $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
257
                    } else {
258
                        error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding);
259
                    }
260
                }
261
            }
262
        }
263
264
        $parser = xml_parser_create();
265
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
266
        // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell
267
        // the xml parser to give us back data in the expected charset.
268
        // What if internal encoding is not in one of the 3 allowed?
269
        // we use the broadest one, ie. utf8
270
        // This allows to send data which is native in various charset,
271
        // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding
272 View Code Duplication
        if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
            xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
274
        } else {
275
            xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
276
        }
277
278
        $xmlRpcParser = new XMLParser();
279
        xml_set_object($parser, $xmlRpcParser);
280
281
        if ($returnType == 'phpvals') {
282
            xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
283
        } else {
284
            xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
285
        }
286
287
        xml_set_character_data_handler($parser, 'xmlrpc_cd');
288
        xml_set_default_handler($parser, 'xmlrpc_dh');
289
290
        // first error check: xml not well formed
291
        if (!xml_parse($parser, $data, count($data))) {
292
            // thanks to Peter Kocks <[email protected]>
293
            if ((xml_get_current_line_number($parser)) == 1) {
294
                $errStr = 'XML error at line 1, check URL';
295
            } else {
296
                $errStr = sprintf('XML error: %s at line %d, column %d',
297
                    xml_error_string(xml_get_error_code($parser)),
298
                    xml_get_current_line_number($parser), xml_get_current_column_number($parser));
299
            }
300
            error_log($errStr);
301
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' (' . $errStr . ')');
302
            xml_parser_free($parser);
303
            if ($this->debug) {
304
                print $errStr;
305
            }
306
            $r->hdrs = $this->httpResponse['headers'];
307
            $r->_cookies = $this->httpResponse['cookies'];
308
            $r->raw_data = $this->httpResponse['raw_data'];
309
310
            return $r;
311
        }
312
        xml_parser_free($parser);
313
        // second error check: xml well formed but not xml-rpc compliant
314
        if ($xmlRpcParser->_xh['isf'] > 1) {
315
            if ($this->debug) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
316
                /// @todo echo something for user?
317
            }
318
319
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
320
                PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
321
        }
322
        // third error check: parsing of the response has somehow gone boink.
323
        // NB: shall we omit this check, since we trust the parsing code?
324
        elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) {
325
            // something odd has happened
326
            // and it's time to generate a client side error
327
            // indicating something odd went on
328
            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
329
                PhpXmlRpc::$xmlrpcstr['invalid_return']);
330
        } else {
331
            if ($this->debug > 1) {
332
                Logger::instance()->debugMessage(
333
                    "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
334
                );
335
            }
336
337
            // note that using =& will raise an error if $xmlRpcParser->_xh['st'] does not generate an object.
338
            $v = &$xmlRpcParser->_xh['value'];
339
340
            if ($xmlRpcParser->_xh['isf']) {
341
                /// @todo we should test here if server sent an int and a string, and/or coerce them into such...
342
                if ($returnType == 'xmlrpcvals') {
343
                    $errNo_v = $v['faultCode'];
344
                    $errStr_v = $v['faultString'];
345
                    $errNo = $errNo_v->scalarval();
346
                    $errStr = $errStr_v->scalarval();
347
                } else {
348
                    $errNo = $v['faultCode'];
349
                    $errStr = $v['faultString'];
350
                }
351
352
                if ($errNo == 0) {
353
                    // FAULT returned, errno needs to reflect that
354
                    $errNo = -1;
355
                }
356
357
                $r = new Response(0, $errNo, $errStr);
358
            } else {
359
                $r = new Response($v, 0, '', $returnType);
360
            }
361
        }
362
363
        $r->hdrs = $this->httpResponse['headers'];
364
        $r->_cookies = $this->httpResponse['cookies'];
365
        $r->raw_data = $this->httpResponse['raw_data'];
366
367
        return $r;
368
    }
369
370
    /**
371
     * Kept the old name even if Request class was renamed, for compatibility.
372
     *
373
     * @return string
374
     */
375
    public function kindOf()
376
    {
377
        return 'msg';
378
    }
379
380
    /**
381
     * Enables/disables the echoing to screen of the xmlrpc responses received.
382
     *
383
     * @param integer $in values 0, 1, 2 are supported
384
     */
385
    public function setDebug($in)
386
    {
387
        $this->debug = $in;
388
    }
389
}
390