Skip to content

Commit

Permalink
Moved the response methods from HttpSocket to HttpResponse.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrbasso committed Dec 14, 2010
1 parent 60a9d34 commit 176da15
Show file tree
Hide file tree
Showing 4 changed files with 486 additions and 447 deletions.
249 changes: 249 additions & 0 deletions cake/libs/http_response.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ class HttpResponse implements ArrayAccess {
*/
public $raw = '';

/**
* Contructor
*
*/
public function __construct($message = null) {
if ($message !== null) {
$this->parseResponse($message);
}
}

/**
* Body content
*
Expand Down Expand Up @@ -105,6 +115,245 @@ public function isOk() {
return $this->code == 200;
}

/**
* Parses the given message and breaks it down in parts.
*
* @param string $message Message to parse
* @return void
* @throw Exception
*/
public function parseResponse($message) {
if (!is_string($message)) {
throw new Exception(__('Invalid response.'));
}

if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
throw new Exception(__('Invalid HTTP response.'));
}

list(, $statusLine, $header) = $match;
$this->raw = $message;
$this->body = (string)substr($message, strlen($match[0]));

if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
$this->httpVersion = $match[1];
$this->code = $match[2];
$this->reasonPhrase = $match[3];
}

$this->headers = $this->_parseHeader($header);
$transferEncoding = $this->getHeader('Transfer-Encoding');
$decoded = $this->_decodeBody($this->body, $transferEncoding);
$this->body = $decoded['body'];

if (!empty($decoded['header'])) {
$this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
}

if (!empty($this->headers)) {
$this->cookies = $this->parseCookies($this->headers);
}
}

/**
* Generic function to decode a $body with a given $encoding. Returns either an array with the keys
* 'body' and 'header' or false on failure.
*
* @param string $body A string continaing the body to decode.
* @param mixed $encoding Can be false in case no encoding is being used, or a string representing the encoding.
* @return mixed Array of response headers and body or false.
*/
protected function _decodeBody($body, $encoding = 'chunked') {
if (!is_string($body)) {
return false;
}
if (empty($encoding)) {
return array('body' => $body, 'header' => false);
}
$decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';

if (!is_callable(array(&$this, $decodeMethod))) {
return array('body' => $body, 'header' => false);
}
return $this->{$decodeMethod}($body);
}

/**
* Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
* a result.
*
* @param string $body A string continaing the chunked body to decode.
* @return mixed Array of response headers and body or false.
* @throws Exception
*/
protected function _decodeChunkedBody($body) {
if (!is_string($body)) {
return false;
}

$decodedBody = null;
$chunkLength = null;

while ($chunkLength !== 0) {

This comment has been minimized.

Copy link
@challet

challet Nov 8, 2011

Contributor

Some providers don't strictly follow the Chunked Transfer Encoding by putting only a \n at the end of the Chunk header.
Don't know if it should be taken into account here, but sometimes I have to use that regexp :
/^([0-9a-f]+) *(?:;(.+)=(.+))?[\r\n]{1,2}/iU

And sorry I didn't find the original commit of that line

This comment has been minimized.

Copy link
@markstory

markstory Nov 9, 2011

Member

That specific pattern causes two existing tests to fail, however /^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU works with improper and proper line endings.

This comment has been minimized.

Copy link
@challet

challet Nov 9, 2011

Contributor

Alright, thanks for the improvement. And sorry to not have checked the tests.
Do you think I should do a pull request on that one ?

edit : ok, you already changed it here Thanks !

if (!preg_match("/^([0-9a-f]+) *(?:;(.+)=(.+))?\r\n/iU", $body, $match)) {
throw new Exception(__('HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
}

$chunkSize = 0;
$hexLength = 0;
$chunkExtensionName = '';
$chunkExtensionValue = '';
if (isset($match[0])) {
$chunkSize = $match[0];
}
if (isset($match[1])) {
$hexLength = $match[1];
}
if (isset($match[2])) {
$chunkExtensionName = $match[2];
}
if (isset($match[3])) {
$chunkExtensionValue = $match[3];
}

$body = substr($body, strlen($chunkSize));
$chunkLength = hexdec($hexLength);
$chunk = substr($body, 0, $chunkLength);
if (!empty($chunkExtensionName)) {
/**
* @todo See if there are popular chunk extensions we should implement
*/
}
$decodedBody .= $chunk;
if ($chunkLength !== 0) {

This comment has been minimized.

Copy link
@challet

challet Nov 9, 2011

Contributor

Since the $chunkLength value is a number of bytes (as specified in the RFC : "chunk-data = chunk-size(OCTET)"), the use of substr could lead to remove some extra bytes and broke the decoding :

Testing case :

  • mbstring.func_overload & 2 == 2 (internally replace substr to mb_substr)
  • the $body variable contains at least one multibyte character (é for instance).

I would suggest the use of mb_strcut ([...] operates on bytes instead of characters.) as a replacement.

Note : This is a very specific situation. Since the basic strlen (without overloading) works as "1 byte == 1 character", it would still works for multi-bytes string. The suggestion given is fixing for the specific case without to broke the general one.

$body = substr($body, $chunkLength + strlen("\r\n"));
}
}

$entityHeader = false;
if (!empty($body)) {
$entityHeader = $this->_parseHeader($body);
}
return array('body' => $decodedBody, 'header' => $entityHeader);
}

/**
* Parses an array based header.
*
* @param array $header Header as an indexed array (field => value)
* @return array Parsed header
*/
protected function _parseHeader($header) {
if (is_array($header)) {
return $header;
} elseif (!is_string($header)) {
return false;
}

preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);

$header = array();
foreach ($matches as $match) {
list(, $field, $value) = $match;

$value = trim($value);
$value = preg_replace("/[\t ]\r\n/", "\r\n", $value);

$field = $this->_unescapeToken($field);

if (!isset($header[$field])) {
$header[$field] = $value;
} else {
$header[$field] = array_merge((array)$header[$field], (array)$value);
}
}
return $header;
}

/**
* Parses cookies in response headers.
*
* @param array $header Header array containing one ore more 'Set-Cookie' headers.
* @return mixed Either false on no cookies, or an array of cookies recieved.
* @todo Make this 100% RFC 2965 confirm
*/
public function parseCookies($header) {
if (!isset($header['Set-Cookie'])) {
return false;
}

$cookies = array();
foreach ((array)$header['Set-Cookie'] as $cookie) {
if (strpos($cookie, '";"') !== false) {
$cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
$parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
} else {
$parts = preg_split('/\;[ \t]*/', $cookie);
}

list($name, $value) = explode('=', array_shift($parts), 2);
$cookies[$name] = compact('value');

foreach ($parts as $part) {
if (strpos($part, '=') !== false) {
list($key, $value) = explode('=', $part);
} else {
$key = $part;
$value = true;
}

$key = strtolower($key);
if (!isset($cookies[$name][$key])) {
$cookies[$name][$key] = $value;
}
}
}
return $cookies;
}

/**
* Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
*
* @param string $token Token to unescape
* @param array $chars
* @return string Unescaped token
* @todo Test $chars parameter
*/
protected function _unescapeToken($token, $chars = null) {
$regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
$token = preg_replace($regex, '\\1', $token);
return $token;
}

/**
* Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
*
* @param boolean $hex true to get them as HEX values, false otherwise
* @param array $chars
* @return array Escape chars
* @todo Test $chars parameter
*/
protected function _tokenEscapeChars($hex = true, $chars = null) {
if (!empty($chars)) {
$escape = $chars;
} else {
$escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
for ($i = 0; $i <= 31; $i++) {
$escape[] = chr($i);
}
$escape[] = chr(127);
}

if ($hex == false) {
return $escape;
}
$regexChars = '';
foreach ($escape as $key => $char) {
$escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
}
return $escape;
}

/**
* ArrayAccess - Offset Exists
*
Expand Down
Loading

0 comments on commit 176da15

Please sign in to comment.