make it possible to retrieve from Response error http codes; introduce custom excepti...
authorgggeek <giunta.gaetano@gmail.com>
Thu, 9 Dec 2021 18:18:29 +0000 (18:18 +0000)
committergggeek <giunta.gaetano@gmail.com>
Thu, 9 Dec 2021 18:18:29 +0000 (18:18 +0000)
NEWS
lib/xmlrpc.inc
src/Client.php
src/Exception/HttpException.php [new file with mode: 0644]
src/Exception/PhpXmlrpcException.php [new file with mode: 0644]
src/Helper/Http.php
src/Request.php
src/Response.php
src/Server.php

diff --git a/NEWS b/NEWS
index 34bdcd8..5ff12fc 100644 (file)
--- 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
 
 
index 8059405..e9338a2 100644 (file)
@@ -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');
index 6d71c01..cd1491c 100644 (file)
@@ -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 (file)
index 0000000..5b9322e
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class HttpException extends PhpXmlrpcException
+{
+    protected $statusCode;
+
+    public function __construct($message = "", $code = 0, $previous = null, $statusCode = null)
+    {
+        parent::__construct($message, $code, $previous);
+        $this->statusCode = $statusCode;
+    }
+
+    public function statusCode()
+    {
+        return $this->statusCode;
+    }
+}
diff --git a/src/Exception/PhpXmlrpcException.php b/src/Exception/PhpXmlrpcException.php
new file mode 100644 (file)
index 0000000..e143f4e
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class PhpXmlrpcException extends \Exception
+{
+}
index ecf2d48..e3d0364 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace PhpXmlRpc\Helper;
 
+use PhpXmlRpc\Exception\HttpException;
 use PhpXmlRpc\PhpXmlRpc;
 
 class Http
@@ -66,12 +67,12 @@ class Http
      * @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
+     * @return array with keys 'headers', 'cookies', 'raw_data' and 'status_code'
+     * @throws HttpException
      */
     public function parseResponseHeaders(&$data, $headersProcessed = false, $debug=0)
     {
-        $httpResponse = array('raw_data' => $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']);
                     }
                 }
             }
index 01fc9d6..9841c01 100644 (file)
@@ -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;
     }
 
index e634ce5..f814d7b 100644 (file)
@@ -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);
+        }
+    }
 }
index e859c7b..1cc965f 100644 (file)
@@ -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;
                 }