01fc9d660c186cf0047e29d806fe41f4bb72f1dd
[plcapi.git] / src / Request.php
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     public function getLogger()
33     {
34         if (self::$logger === null) {
35             self::$logger = Logger::instance();
36         }
37         return self::$logger;
38     }
39
40     public static function setLogger($logger)
41     {
42         self::$logger = $logger;
43     }
44
45     public function getParser()
46     {
47         if (self::$parser === null) {
48             self::$parser = new XMLParser();
49         }
50         return self::$parser;
51     }
52
53     public static function setParser($parser)
54     {
55         self::$parser = $parser;
56     }
57
58     public function getCharsetEncoder()
59     {
60         if (self::$charsetEncoder === null) {
61             self::$charsetEncoder = Charset::instance();
62         }
63         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     public function __construct($methodName, $params = array())
76     {
77         $this->methodname = $methodName;
78         foreach ($params as $param) {
79             $this->addParam($param);
80         }
81     }
82
83     /**
84      * @internal this function will become protected in the future
85      * @param string $charsetEncoding
86      * @return string
87      */
88     public function xml_header($charsetEncoding = '')
89     {
90         if ($charsetEncoding != '') {
91             return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
92         } else {
93             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     public function xml_footer()
102     {
103         return '</methodCall>';
104     }
105
106     /**
107      * @internal this function will become protected in the future
108      * @param string $charsetEncoding
109      */
110     public function createPayload($charsetEncoding = '')
111     {
112         if ($charsetEncoding != '') {
113             $this->content_type = 'text/xml; charset=' . $charsetEncoding;
114         } else {
115             $this->content_type = 'text/xml';
116         }
117         $this->payload = $this->xml_header($charsetEncoding);
118         $this->payload .= '<methodName>' . $this->getCharsetEncoder()->encodeEntities(
119             $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
120         $this->payload .= "<params>\n";
121         foreach ($this->params as $p) {
122             $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) .
123                 "</param>\n";
124         }
125         $this->payload .= "</params>\n";
126         $this->payload .= $this->xml_footer();
127     }
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     public function method($methodName = '')
137     {
138         if ($methodName != '') {
139             $this->methodname = $methodName;
140         }
141
142         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     public function serialize($charsetEncoding = '')
153     {
154         $this->createPayload($charsetEncoding);
155
156         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     public function addParam($param)
169     {
170         // check: do not add to self params which are not xmlrpc values
171         if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
172             $this->params[] = $param;
173
174             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     public function getParam($i)
188     {
189         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     public function getNumParams()
198     {
199         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     public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals')
241     {
242         if ($this->debug) {
243             $this->getLogger()->debugMessage("---GOT---\n$data\n---END---");
244         }
245
246         $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
247
248         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         if (substr($data, 0, 4) == 'HTTP') {
255             $httpParser = new Http();
256             try {
257                 $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug);
258             } catch(\Exception $e) {
259                 $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                 $r->raw_data = $data;
263
264                 return $r;
265             }
266         }
267
268         // be tolerant of extra whitespace in response body
269         $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 <luca.mariano@email.it> originally in PEARified version of the lib
275         $pos = strrpos($data, '</methodResponse>');
276         if ($pos !== false) {
277             $data = substr($data, 0, $pos + 17);
278         }
279
280         // try to 'guestimate' the character encoding of the received response
281         $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data);
282
283         if ($this->debug) {
284             $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
285             if ($start) {
286                 $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
287                 $end = strpos($data, '-->', $start);
288                 $comments = substr($data, $start, $end - $start);
289                 $this->getLogger()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
290                     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         if ($returnType == 'xml') {
296             $r = new Response($data, 0, '', 'xml');
297             $r->hdrs = $this->httpResponse['headers'];
298             $r->_cookies = $this->httpResponse['cookies'];
299             $r->raw_data = $this->httpResponse['raw_data'];
300
301             return $r;
302         }
303
304         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             if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
311                 if ($respEncoding == 'ISO-8859-1') {
312                     $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         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             $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
333         }
334
335         $xmlRpcParser = $this->getParser();
336         $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
337
338         // first error check: xml not well formed
339         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             $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
344                 PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
345
346             if ($this->debug) {
347                 print $xmlRpcParser->_xh['isf_reason'];
348             }
349         }
350         // second error check: xml well formed but not xml-rpc compliant
351         elseif ($xmlRpcParser->_xh['isf'] == 2) {
352             $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
353                 PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
354
355             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         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             if ($this->debug > 1) {
369                 $this->getLogger()->debugMessage(
370                     "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
371                 );
372             }
373
374             $v = $xmlRpcParser->_xh['value'];
375
376             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                 if ($returnType == 'xmlrpcvals') {
379                     $errNo_v = $v['faultCode'];
380                     $errStr_v = $v['faultString'];
381                     $errNo = $errNo_v->scalarval();
382                     $errStr = $errStr_v->scalarval();
383                 } else {
384                     $errNo = $v['faultCode'];
385                     $errStr = $v['faultString'];
386                 }
387
388                 if ($errNo == 0) {
389                     // FAULT returned, errno needs to reflect that
390                     $errNo = -1;
391                 }
392
393                 $r = new Response(0, $errNo, $errStr);
394             } else {
395                 $r = new Response($v, 0, '', $returnType);
396             }
397         }
398
399         $r->hdrs = $this->httpResponse['headers'];
400         $r->_cookies = $this->httpResponse['cookies'];
401         $r->raw_data = $this->httpResponse['raw_data'];
402
403         return $r;
404     }
405
406     /**
407      * Kept the old name even if Request class was renamed, for compatibility.
408      *
409      * @return string
410      */
411     public function kindOf()
412     {
413         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     public function setDebug($level)
422     {
423         $this->debug = $level;
424     }
425 }