From bcf72dda17486e2800b66c4527bc4b5f4d54fc7a Mon Sep 17 00:00:00 2001 From: gggeek Date: Thu, 9 Dec 2021 18:18:29 +0000 Subject: [PATCH] make it possible to retrieve from Response error http codes; introduce custom exceptions; deprecations and cleanups --- NEWS | 11 ++++ lib/xmlrpc.inc | 2 + src/Client.php | 8 +-- src/Exception/HttpException.php | 19 ++++++ src/Exception/PhpXmlrpcException.php | 7 ++ src/Helper/Http.php | 29 +++++---- src/Request.php | 45 ++++++------- src/Response.php | 97 ++++++++++++++++++++++++++-- src/Server.php | 20 ++++-- 9 files changed, 187 insertions(+), 51 deletions(-) create mode 100644 src/Exception/HttpException.php create mode 100644 src/Exception/PhpXmlrpcException.php diff --git a/NEWS b/NEWS index 34bdcd8..5ff12fc 100644 --- a/NEWS +++ b/NEWS @@ -21,6 +21,17 @@ XML-RPC for PHP version 4.6.0 - 2021/12/9 * new: method `Wrapper::wrapPhpClass` allows to customize the names of the phpxmlrpc methods by stripping the original class name and accompanying namespace and replace it with a user-defined prefix, via option `replace_class_name` +* new: `Response` constructor gained a 4th argument + +* deprecated: properties `Response::hdrs`, `Response::_cookies`, `Response::raw_data`. Use `Response::httpResponse()` instead. + That method returns an array which also holds the http response's status code - useful in case of http errors. + +* deprecated: method `Request::createPayload`. Use `Request::serialize` instead + +* deprecated: property `Request::httpResponse` + +* improved: `Http::parseResponseHeaders` now throws a more specific exception in case of http errors + * improved: Continuous Integration is now running on Github Actions instead of Travis diff --git a/lib/xmlrpc.inc b/lib/xmlrpc.inc index 8059405..e9338a2 100644 --- a/lib/xmlrpc.inc +++ b/lib/xmlrpc.inc @@ -54,6 +54,8 @@ include_once(__DIR__.'/../src/PhpXmlRpc.php'); include_once(__DIR__.'/../src/Request.php'); include_once(__DIR__.'/../src/Response.php'); include_once(__DIR__.'/../src/Value.php'); +include_once(__DIR__.'/../src/Exception/HttpException.php'); +include_once(__DIR__.'/../src/Exception/PhpXmlrpcException.php'); include_once(__DIR__.'/../src/Helper/Charset.php'); include_once(__DIR__.'/../src/Helper/Date.php'); include_once(__DIR__.'/../src/Helper/Http.php'); diff --git a/src/Client.php b/src/Client.php index 6d71c01..cd1491c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -3,7 +3,7 @@ namespace PhpXmlRpc; use PhpXmlRpc\Helper\Logger; - +use PhpXmlRpc\Helper\XMLParser; /** * Used to represent a client of an XML-RPC server. */ @@ -111,7 +111,7 @@ class Client * response will be lost. It will be e.g. impossible to tell whether a particular php string value was sent by the * server as an xmlrpc string or base64 value. */ - public $return_type = 'xmlrpcvals'; + public $return_type = XMLParser::RETURN_XMLRPCVALS; /** * Sent to servers in http headers. @@ -659,7 +659,7 @@ class Client // Only create the payload if it was not created previously if (empty($req->payload)) { - $req->createPayload($this->request_charset_encoding); + $req->serialize($this->request_charset_encoding); } $payload = $req->payload; @@ -894,7 +894,7 @@ class Client // Only create the payload if it was not created previously if (empty($req->payload)) { - $req->createPayload($this->request_charset_encoding); + $req->serialize($this->request_charset_encoding); } // Deflate request body and set appropriate request headers diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php new file mode 100644 index 0000000..5b9322e --- /dev/null +++ b/src/Exception/HttpException.php @@ -0,0 +1,19 @@ +statusCode = $statusCode; + } + + public function statusCode() + { + return $this->statusCode; + } +} diff --git a/src/Exception/PhpXmlrpcException.php b/src/Exception/PhpXmlrpcException.php new file mode 100644 index 0000000..e143f4e --- /dev/null +++ b/src/Exception/PhpXmlrpcException.php @@ -0,0 +1,7 @@ + $data, 'headers'=> array(), 'cookies' => array()); + $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array(), 'status_code' => null); // Support "web-proxy-tunnelling" connections for https through proxies if (preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data)) { @@ -95,7 +96,7 @@ class Http $data = substr($data, $bd); } else { Logger::instance()->errorLog('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']); + throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']); } } @@ -114,16 +115,20 @@ class Http // When using Curl to query servers using Digest Auth, we get back a double set of http headers. // We strip out the 1st... - if ($headersProcessed && preg_match('/^HTTP\/[0-9.]+ 401 /', $data)) { - if (preg_match('/(\r?\n){2}HTTP\/[0-9.]+ 200 /', $data)) { - $data = preg_replace('/^HTTP\/[0-9.]+ 401 .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1); + if ($headersProcessed && preg_match('/^HTTP\/[0-9]\.[0-9] 401 /', $data)) { + if (preg_match('/(\r?\n){2}HTTP\/[0-9]\.[0-9] 200 /', $data)) { + $data = preg_replace('/^HTTP\/[0-9]\.[0-9] 401 .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1); } } - if (!preg_match('/^HTTP\/[0-9.]+ 200 /', $data)) { + if (preg_match('/^HTTP\/[0-9]\.[0-9] ([0-9]{3}) /', $data, $matches)) { + $httpResponse['status_code'] = $matches[1]; + } + + if ($httpResponse['status_code'] !== '200') { $errstr = substr($data, 0, strpos($data, "\n") - 1); Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr); - throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']); + throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error'], null, $httpResponse['status_code'] ); } // be tolerant to usage of \n instead of \r\n to separate headers and data @@ -220,7 +225,7 @@ class Http if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') { if (!$data = static::decodeChunked($data)) { Logger::instance()->errorLog('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']); + throw new HttpException(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail'], null, $httpResponse['status_code']); } } @@ -243,11 +248,11 @@ class Http } } else { Logger::instance()->errorLog('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']); + throw new HttpException(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail'], null, $httpResponse['status_code']); } } else { Logger::instance()->errorLog('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']); + throw new HttpException(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress'], null, $httpResponse['status_code']); } } } diff --git a/src/Request.php b/src/Request.php index 01fc9d6..9841c01 100644 --- a/src/Request.php +++ b/src/Request.php @@ -2,6 +2,7 @@ namespace PhpXmlRpc; +use PhpXmlRpc\Exception\HttpException; use PhpXmlRpc\Helper\Charset; use PhpXmlRpc\Helper\Http; use PhpXmlRpc\Helper\Logger; @@ -27,6 +28,7 @@ class Request public $content_type = 'text/xml'; // holds data while parsing the response. NB: Not a full Response object + /** @deprecated will be removed in a future release */ protected $httpResponse = array(); public function getLogger() @@ -237,7 +239,7 @@ class Request * * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client... */ - public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals') + public function parseResponse($data = '', $headersProcessed = false, $returnType = XMLParser::RETURN_XMLRPCVALS) { if ($this->debug) { $this->getLogger()->debugMessage("---GOT---\n$data\n---END---"); @@ -255,13 +257,12 @@ class Request $httpParser = new Http(); try { $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug); - } catch(\Exception $e) { - $r = new Response(0, $e->getCode(), $e->getMessage()); + } catch (HttpException $e) { // failed processing of HTTP response headers // save into response obj the full payload received, for debugging - $r->raw_data = $data; - - return $r; + return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode())); + } catch(\Exception $e) { + return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data)); } } @@ -293,12 +294,7 @@ class Request // if user wants back raw xml, give it to her 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; + return new Response($data, 0, '', 'xml', $this->httpResponse); } if ($respEncoding != '') { @@ -341,7 +337,9 @@ class Request // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL' $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], - PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']); + PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '', + $this->httpResponse + ); if ($this->debug) { print $xmlRpcParser->_xh['isf_reason']; @@ -350,7 +348,9 @@ class Request // second error check: xml well formed but not xml-rpc compliant elseif ($xmlRpcParser->_xh['isf'] == 2) { $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], - PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']); + PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '', + $this->httpResponse + ); if ($this->debug) { /// @todo echo something for user? @@ -358,12 +358,13 @@ class Request } // third error check: parsing of the response has somehow gone boink. /// @todo shall we omit this check, since we trust the parsing code? - elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) { + elseif ($returnType == XMLParser::RETURN_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']); + $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'], + '', $this->httpResponse + ); } else { if ($this->debug > 1) { $this->getLogger()->debugMessage( @@ -375,7 +376,7 @@ class Request 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') { + if ($returnType == XMLParser::RETURN_XMLRPCVALS) { $errNo_v = $v['faultCode']; $errStr_v = $v['faultString']; $errNo = $errNo_v->scalarval(); @@ -390,16 +391,12 @@ class Request $errNo = -1; } - $r = new Response(0, $errNo, $errStr); + $r = new Response(0, $errNo, $errStr, '', $this->httpResponse); } else { - $r = new Response($v, 0, '', $returnType); + $r = new Response($v, 0, '', $returnType, $this->httpResponse); } } - $r->hdrs = $this->httpResponse['headers']; - $r->_cookies = $this->httpResponse['cookies']; - $r->raw_data = $this->httpResponse['raw_data']; - return $r; } diff --git a/src/Response.php b/src/Response.php index e634ce5..f814d7b 100644 --- a/src/Response.php +++ b/src/Response.php @@ -8,6 +8,10 @@ use PhpXmlRpc\Helper\Charset; * This class provides the representation of the response of an XML-RPC server. * Server-side, a server method handler will construct a Response and pass it as its return value. * An identical Response object will be returned by the result of an invocation of the send() method of the Client class. + * + * @property array $hdrs deprecated, use $httpResponse['headers'] + * @property array _cookies deprecated, use $httpResponse['cookies'] + * @property string $raw_data deprecated, use $httpResponse['raw_data'] */ class Response { @@ -24,9 +28,7 @@ class Response public $errstr = ''; public $payload; public $content_type = 'text/xml'; - public $hdrs = array(); - public $_cookies = array(); - public $raw_data = ''; + protected $httpResponse = array('headers' => array(), 'cookies' => array(), 'raw_data' => '', 'status_code' => null); public function getCharsetEncoder() { @@ -47,12 +49,13 @@ class Response * @param string $fString the error string, in case of an error response * @param string $valType The type of $val passed in. Either 'xmlrpcvals', 'phpvals' or 'xml'. Leave empty to let * the code guess the correct type. + * @param array|null $httpResponse * * @todo add check that $val / $fCode / $fString is of correct type??? * NB: as of now we do not do it, since it might be either an xmlrpc value or a plain php val, or a complete * xml chunk, depending on usage of Client::send() inside which creator is called... */ - public function __construct($val, $fCode = 0, $fString = '', $valType = '') + public function __construct($val, $fCode = 0, $fString = '', $valType = '', $httpResponse = null) { if ($fCode != 0) { // error response @@ -75,6 +78,10 @@ class Response $this->valtyp = $valType; } } + + if (is_array($httpResponse)) { + $this->httpResponse = array_merge(array('headers' => array(), 'cookies' => array(), 'raw_data' => '', 'status_code' => null), $httpResponse); + } } /** @@ -121,7 +128,15 @@ class Response */ public function cookies() { - return $this->_cookies; + return $this->httpResponse['cookies']; + } + + /** + * @return array array with keys 'headers', 'cookies', 'raw_data' and 'status_code' + */ + public function httpResponse() + { + return $this->httpResponse; } /** @@ -173,4 +188,76 @@ class Response return $result; } + + // BC layer + + public function __get($name) + { + //trigger_error('getting property Response::' . $name . ' is deprecated', E_USER_DEPRECATED); + + switch($name) { + case 'hdrs': + return $this->httpResponse['headers']; + case '_cookies': + return $this->httpResponse['cookies']; + case 'raw_data': + return $this->httpResponse['raw_data']; + default: + $trace = debug_backtrace(); + trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); + return null; + } + } + + public function __set($name, $value) + { + //trigger_error('setting property Response::' . $name . ' is deprecated', E_USER_DEPRECATED); + + switch($name) { + case 'hdrs': + $this->httpResponse['headers'] = $value; + break; + case '_cookies': + $this->httpResponse['cookies'] = $value; + break; + case 'raw_data': + $this->httpResponse['raw_data'] = $value; + break; + default: + $trace = debug_backtrace(); + trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); + } + } + + public function __isset($name) + { + switch($name) { + case 'hdrs': + return isset($this->httpResponse['headers']); + case '_cookies': + return isset($this->httpResponse['cookies']); + case 'raw_data': + return isset($this->httpResponse['raw_data']); + default: + return false; + } + } + + public function __unset($name) + { + switch($name) { + case 'hdrs': + unset($this->httpResponse['headers']); + break; + case '_cookies': + unset($this->httpResponse['cookies']); + break; + case 'raw_data': + unset($this->httpResponse['raw_data']); + break; + default: + $trace = debug_backtrace(); + trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); + } + } } diff --git a/src/Server.php b/src/Server.php index e859c7b..1cc965f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -275,10 +275,12 @@ class Server if (!$r) { // this actually executes the request $r = $this->parseRequest($data, $reqCharset); - } - // save full body of request into response, for more debugging usages - $r->raw_data = $rawData; + // save full body of request into response, for more debugging usages. + // Note that this is the _request_ data, not the response's own data, unlike what happens client-side + /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method + $r->raw_data = $rawData; + } if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors) { $this->debugmsg("+++PROCESSING ERRORS AND WARNINGS+++\n" . @@ -428,7 +430,7 @@ class Server /** * Parse http headers received along with xmlrpc request. If needed, inflate request. * - * @return mixed Response|null on success or an error Response + * @return Response|null null on success or an error Response */ protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression) { @@ -453,6 +455,8 @@ class Server $contentEncoding = ''; } + $rawData = $data; + // check if request body has been compressed and decompress it if ($contentEncoding != '' && strlen($data)) { if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') { @@ -469,12 +473,16 @@ class Server $this->debugmsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++"); } } else { - $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'], PhpXmlRpc::$xmlrpcstr['server_decompress_fail']); + $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'], + PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData) + ); return $r; } } else { - $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'], PhpXmlRpc::$xmlrpcstr['server_cannot_decompress']); + $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'], + PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData) + ); return $r; } -- 2.43.0