-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,6 +69,16 @@ class HttpResponse implements ArrayAccess { | |
*/ | ||
public $raw = ''; | ||
|
||
/** | ||
* Contructor | ||
* | ||
*/ | ||
public function __construct($message = null) { | ||
if ($message !== null) { | ||
$this->parseResponse($message); | ||
} | ||
} | ||
|
||
/** | ||
* Body content | ||
* | ||
|
@@ -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.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
markstory
Member
|
||
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.
Sorry, something went wrong.
challet
Contributor
|
||
$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 | ||
* | ||
|
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