/** * Parse the xmlrpc response contained in the string $data and return a Response object. * * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding. * * @param string $data the xmlrpc response, possibly including http headers * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and * consequent decoding * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or * 'phpvals' * * @return Response */ public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals') { if ($this->debug) { Logger::instance()->debugMessage("---GOT---\n{$data}\n---END---"); } $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array()); if ($data == '') { error_log('XML-RPC: ' . __METHOD__ . ': no response received from server.'); return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']); } // parse the HTTP headers of the response, if present, and separate them from data if (substr($data, 0, 4) == 'HTTP') { $httpParser = new Http(); try { $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug); } catch (\Exception $e) { $r = new Response(0, $e->getCode(), $e->getMessage()); // failed processing of HTTP response headers // save into response obj the full payload received, for debugging $r->raw_data = $data; return $r; } } // be tolerant of extra whitespace in response body $data = trim($data); /// @todo return an error msg if $data=='' ? // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts) // idea from Luca Mariano <*****@*****.**> originally in PEARified version of the lib $pos = strrpos($data, '</methodResponse>'); if ($pos !== false) { $data = substr($data, 0, $pos + 17); } // try to 'guestimate' the character encoding of the received response $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data); if ($this->debug) { $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); if ($start) { $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); $end = strpos($data, '-->', $start); $comments = substr($data, $start, $end - $start); Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" . str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding); } } // if user wants back raw xml, give it to him if ($returnType == 'xml') { $r = new Response($data, 0, '', 'xml'); $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; } if ($respEncoding != '') { // Since parsing will fail if charset is not specified in the xml prologue, // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that... // The following code might be better for mb_string enabled installs, but // makes the lib about 200% slower... //if (!is_valid_charset($respEncoding, array('UTF-8'))) if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) { if ($respEncoding == 'ISO-8859-1') { $data = utf8_encode($data); } else { if (extension_loaded('mbstring')) { $data = mb_convert_encoding($data, 'UTF-8', $respEncoding); } else { error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding); } } } } $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true); // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell // the xml parser to give us back data in the expected charset. // What if internal encoding is not in one of the 3 allowed? // we use the broadest one, ie. utf8 // This allows to send data which is native in various charset, // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); } else { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding); } $xmlRpcParser = new XMLParser(); xml_set_object($parser, $xmlRpcParser); if ($returnType == 'phpvals') { xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast'); } else { xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee'); } xml_set_character_data_handler($parser, 'xmlrpc_cd'); xml_set_default_handler($parser, 'xmlrpc_dh'); // first error check: xml not well formed if (!xml_parse($parser, $data, count($data))) { // thanks to Peter Kocks <*****@*****.**> if (xml_get_current_line_number($parser) == 1) { $errStr = 'XML error at line 1, check URL'; } else { $errStr = sprintf('XML error: %s at line %d, column %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser), xml_get_current_column_number($parser)); } error_log($errStr); $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' (' . $errStr . ')'); xml_parser_free($parser); if ($this->debug) { print $errStr; } $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; } xml_parser_free($parser); // second error check: xml well formed but not xml-rpc compliant if ($xmlRpcParser->_xh['isf'] > 1) { if ($this->debug) { /// @todo echo something for user? } $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']); } elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) { // something odd has happened // and it's time to generate a client side error // indicating something odd went on $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return']); } else { if ($this->debug > 1) { Logger::instance()->debugMessage("---PARSED---\n" . var_export($xmlRpcParser->_xh['value'], true) . "\n---END---"); } // note that using =& will raise an error if $xmlRpcParser->_xh['st'] does not generate an object. $v =& $xmlRpcParser->_xh['value']; if ($xmlRpcParser->_xh['isf']) { /// @todo we should test here if server sent an int and a string, and/or coerce them into such... if ($returnType == 'xmlrpcvals') { $errNo_v = $v['faultCode']; $errStr_v = $v['faultString']; $errNo = $errNo_v->scalarval(); $errStr = $errStr_v->scalarval(); } else { $errNo = $v['faultCode']; $errStr = $v['faultString']; } if ($errNo == 0) { // FAULT returned, errno needs to reflect that $errNo = -1; } $r = new Response(0, $errNo, $errStr); } else { $r = new Response($v, 0, '', $returnType); } } $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; }
/** * Parses HTTP an http response headers and separates them from the body. * * @param string $data the http response,headers and body. It will be stripped of headers * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out * * @return array with keys 'headers' and 'cookies' * @throws \Exception */ public function parseResponseHeaders(&$data, $headersProcessed = false, $debug = 0) { $httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array()); // Support "web-proxy-tunelling" connections for https through proxies if (preg_match('/^HTTP\\/1\\.[0-1] 200 Connection established/', $data)) { // Look for CR/LF or simple LF as line separator, // (even though it is not valid http) $pos = strpos($data, "\r\n\r\n"); if ($pos || is_int($pos)) { $bd = $pos + 4; } else { $pos = strpos($data, "\n\n"); if ($pos || is_int($pos)) { $bd = $pos + 2; } else { // No separation between response headers and body: fault? $bd = 0; } } if ($bd) { // this filters out all http headers from proxy. // maybe we could take them into account, too? $data = substr($data, $bd); } else { error_log('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']); } } // Strip HTTP 1.1 100 Continue header if present while (preg_match('/^HTTP\\/1\\.1 1[0-9]{2} /', $data)) { $pos = strpos($data, 'HTTP', 12); // server sent a Continue header without any (valid) content following... // give the client a chance to know it if (!$pos && !is_int($pos)) { // works fine in php 3, 4 and 5 break; } $data = substr($data, $pos); } if (!preg_match('/^HTTP\\/[0-9.]+ 200 /', $data)) { $errstr = substr($data, 0, strpos($data, "\n") - 1); error_log('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr); throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']); } // be tolerant to usage of \n instead of \r\n to separate headers and data // (even though it is not valid http) $pos = strpos($data, "\r\n\r\n"); if ($pos || is_int($pos)) { $bd = $pos + 4; } else { $pos = strpos($data, "\n\n"); if ($pos || is_int($pos)) { $bd = $pos + 2; } else { // No separation between response headers and body: fault? // we could take some action here instead of going on... $bd = 0; } } // be tolerant to line endings, and extra empty lines $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos))); while (list(, $line) = @each($ar)) { // take care of multi-line headers and cookies $arr = explode(':', $line, 2); if (count($arr) > 1) { $headerName = strtolower(trim($arr[0])); /// @todo some other headers (the ones that allow a CSV list of values) /// do allow many values to be passed using multiple header lines. /// We should add content to $xmlrpc->_xh['headers'][$headerName] /// instead of replacing it for those... if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') { if ($headerName == 'set-cookie2') { // version 2 cookies: // there could be many cookies on one line, comma separated $cookies = explode(',', $arr[1]); } else { $cookies = array($arr[1]); } foreach ($cookies as $cookie) { // glue together all received cookies, using a comma to separate them // (same as php does with getallheaders()) if (isset($httpResponse['headers'][$headerName])) { $httpResponse['headers'][$headerName] .= ', ' . trim($cookie); } else { $httpResponse['headers'][$headerName] = trim($cookie); } // parse cookie attributes, in case user wants to correctly honour them // feature creep: only allow rfc-compliant cookie attributes? // @todo support for server sending multiple time cookie with same name, but using different PATHs $cookie = explode(';', $cookie); foreach ($cookie as $pos => $val) { $val = explode('=', $val, 2); $tag = trim($val[0]); $val = trim(@$val[1]); /// @todo with version 1 cookies, we should strip leading and trailing " chars if ($pos == 0) { $cookiename = $tag; $httpResponse['cookies'][$tag] = array(); $httpResponse['cookies'][$cookiename]['value'] = urldecode($val); } else { if ($tag != 'value') { $httpResponse['cookies'][$cookiename][$tag] = $val; } } } } } else { $httpResponse['headers'][$headerName] = trim($arr[1]); } } elseif (isset($headerName)) { /// @todo version1 cookies might span multiple lines, thus breaking the parsing above $httpResponse['headers'][$headerName] .= ' ' . trim($line); } } $data = substr($data, $bd); if ($debug && count($httpResponse['headers'])) { $msg = ''; foreach ($httpResponse['headers'] as $header => $value) { $msg .= "HEADER: {$header}: {$value}\n"; } foreach ($httpResponse['cookies'] as $header => $value) { $msg .= "COOKIE: {$header}={$value['value']}\n"; } Logger::instance()->debugMessage($msg); } // if CURL was used for the call, http headers have been processed, // and dechunking + reinflating have been carried out if (!$headersProcessed) { // Decode chunked encoding sent by http 1.1 servers if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') { if (!($data = Http::decodeChunked($data))) { error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail']); } } // Decode gzip-compressed stuff // code shamelessly inspired from nusoap library by Dietrich Ayala if (isset($httpResponse['headers']['content-encoding'])) { $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']); if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') { // if decoding works, use it. else assume data wasn't gzencoded if (function_exists('gzinflate')) { if ($httpResponse['headers']['content-encoding'] == 'deflate' && ($degzdata = @gzuncompress($data))) { $data = $degzdata; if ($debug) { Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n{$data}\n---END---"); } } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && ($degzdata = @gzinflate(substr($data, 10)))) { $data = $degzdata; if ($debug) { Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n{$data}\n---END---"); } } else { error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail']); } } else { error_log('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress']); } } } } // end of 'if needed, de-chunk, re-inflate response' return $httpResponse; }