* 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
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');
namespace PhpXmlRpc;
use PhpXmlRpc\Helper\Logger;
-
+use PhpXmlRpc\Helper\XMLParser;
/**
* Used to represent a client of an XML-RPC server.
*/
* 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.
// 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;
// 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
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class PhpXmlrpcException extends \Exception
+{
+}
namespace PhpXmlRpc\Helper;
+use PhpXmlRpc\Exception\HttpException;
use PhpXmlRpc\PhpXmlRpc;
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)) {
$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']);
}
}
// 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
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']);
}
}
}
} 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']);
}
}
}
namespace PhpXmlRpc;
+use PhpXmlRpc\Exception\HttpException;
use PhpXmlRpc\Helper\Charset;
use PhpXmlRpc\Helper\Http;
use PhpXmlRpc\Helper\Logger;
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()
*
* @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---");
$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));
}
}
// 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 != '') {
// 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'];
// 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?
}
// 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(
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();
$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;
}
* 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
{
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()
{
* @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
$this->valtyp = $valType;
}
}
+
+ if (is_array($httpResponse)) {
+ $this->httpResponse = array_merge(array('headers' => array(), 'cookies' => array(), 'raw_data' => '', 'status_code' => null), $httpResponse);
+ }
}
/**
*/
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;
}
/**
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);
+ }
+ }
}
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" .
/**
* 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)
{
$contentEncoding = '';
}
+ $rawData = $data;
+
// check if request body has been compressed and decompress it
if ($contentEncoding != '' && strlen($data)) {
if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
$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;
}