1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
19:
20: 21: 22: 23: 24:
25: class HttpResponse implements ArrayAccess {
26:
27: 28: 29: 30: 31:
32: public $body = '';
33:
34: 35: 36: 37: 38:
39: public $headers = array();
40:
41: 42: 43: 44: 45:
46: public $cookies = array();
47:
48: 49: 50: 51: 52:
53: public $httpVersion = 'HTTP/1.1';
54:
55: 56: 57: 58: 59:
60: public $code = 0;
61:
62: 63: 64: 65: 66:
67: public $reasonPhrase = '';
68:
69: 70: 71: 72: 73:
74: public $raw = '';
75:
76: 77: 78: 79: 80:
81: public function __construct($message = null) {
82: if ($message !== null) {
83: $this->parseResponse($message);
84: }
85: }
86:
87: 88: 89: 90: 91:
92: public function body() {
93: return (string)$this->body;
94: }
95:
96: 97: 98: 99: 100: 101: 102:
103: public function getHeader($name, $headers = null) {
104: if (!is_array($headers)) {
105: $headers =& $this->headers;
106: }
107: if (isset($headers[$name])) {
108: return $headers[$name];
109: }
110: foreach ($headers as $key => $value) {
111: if (strcasecmp($key, $name) == 0) {
112: return $value;
113: }
114: }
115: return null;
116: }
117:
118: 119: 120: 121: 122:
123: public function isOk() {
124: return in_array($this->code, array(200, 201, 202, 203, 204, 205, 206));
125: }
126:
127: 128: 129: 130: 131:
132: public function isRedirect() {
133: return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
134: }
135:
136: 137: 138: 139: 140: 141: 142:
143: public function parseResponse($message) {
144: if (!is_string($message)) {
145: throw new SocketException(__d('cake_dev', 'Invalid response.'));
146: }
147:
148: if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
149: throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
150: }
151:
152: list(, $statusLine, $header) = $match;
153: $this->raw = $message;
154: $this->body = (string)substr($message, strlen($match[0]));
155:
156: if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
157: $this->httpVersion = $match[1];
158: $this->code = $match[2];
159: $this->reasonPhrase = $match[3];
160: }
161:
162: $this->headers = $this->_parseHeader($header);
163: $transferEncoding = $this->getHeader('Transfer-Encoding');
164: $decoded = $this->_decodeBody($this->body, $transferEncoding);
165: $this->body = $decoded['body'];
166:
167: if (!empty($decoded['header'])) {
168: $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
169: }
170:
171: if (!empty($this->headers)) {
172: $this->cookies = $this->parseCookies($this->headers);
173: }
174: }
175:
176: 177: 178: 179: 180: 181: 182: 183:
184: protected function _decodeBody($body, $encoding = 'chunked') {
185: if (!is_string($body)) {
186: return false;
187: }
188: if (empty($encoding)) {
189: return array('body' => $body, 'header' => false);
190: }
191: $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
192:
193: if (!is_callable(array(&$this, $decodeMethod))) {
194: return array('body' => $body, 'header' => false);
195: }
196: return $this->{$decodeMethod}($body);
197: }
198:
199: 200: 201: 202: 203: 204: 205: 206:
207: protected function _decodeChunkedBody($body) {
208: if (!is_string($body)) {
209: return false;
210: }
211:
212: $decodedBody = null;
213: $chunkLength = null;
214:
215: while ($chunkLength !== 0) {
216: if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
217: throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
218: }
219:
220: $chunkSize = 0;
221: $hexLength = 0;
222: $chunkExtensionValue = '';
223: if (isset($match[0])) {
224: $chunkSize = $match[0];
225: }
226: if (isset($match[1])) {
227: $hexLength = $match[1];
228: }
229: if (isset($match[3])) {
230: $chunkExtensionValue = $match[3];
231: }
232:
233: $body = substr($body, strlen($chunkSize));
234: $chunkLength = hexdec($hexLength);
235: $chunk = substr($body, 0, $chunkLength);
236: $decodedBody .= $chunk;
237: if ($chunkLength !== 0) {
238: $body = substr($body, $chunkLength + strlen("\r\n"));
239: }
240: }
241:
242: $entityHeader = false;
243: if (!empty($body)) {
244: $entityHeader = $this->_parseHeader($body);
245: }
246: return array('body' => $decodedBody, 'header' => $entityHeader);
247: }
248:
249: 250: 251: 252: 253: 254:
255: protected function _parseHeader($header) {
256: if (is_array($header)) {
257: return $header;
258: } elseif (!is_string($header)) {
259: return false;
260: }
261:
262: preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
263:
264: $header = array();
265: foreach ($matches as $match) {
266: list(, $field, $value) = $match;
267:
268: $value = trim($value);
269: $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
270:
271: $field = $this->_unescapeToken($field);
272:
273: if (!isset($header[$field])) {
274: $header[$field] = $value;
275: } else {
276: $header[$field] = array_merge((array)$header[$field], (array)$value);
277: }
278: }
279: return $header;
280: }
281:
282: 283: 284: 285: 286: 287:
288: public function parseCookies($header) {
289: $cookieHeader = $this->getHeader('Set-Cookie', $header);
290: if (!$cookieHeader) {
291: return false;
292: }
293:
294: $cookies = array();
295: foreach ((array)$cookieHeader as $cookie) {
296: if (strpos($cookie, '";"') !== false) {
297: $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
298: $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
299: } else {
300: $parts = preg_split('/\;[ \t]*/', $cookie);
301: }
302:
303: list($name, $value) = explode('=', array_shift($parts), 2);
304: $cookies[$name] = compact('value');
305:
306: foreach ($parts as $part) {
307: if (strpos($part, '=') !== false) {
308: list($key, $value) = explode('=', $part);
309: } else {
310: $key = $part;
311: $value = true;
312: }
313:
314: $key = strtolower($key);
315: if (!isset($cookies[$name][$key])) {
316: $cookies[$name][$key] = $value;
317: }
318: }
319: }
320: return $cookies;
321: }
322:
323: 324: 325: 326: 327: 328: 329:
330: protected function _unescapeToken($token, $chars = null) {
331: $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
332: $token = preg_replace($regex, '\\1', $token);
333: return $token;
334: }
335:
336: 337: 338: 339: 340: 341: 342:
343: protected function _tokenEscapeChars($hex = true, $chars = null) {
344: if (!empty($chars)) {
345: $escape = $chars;
346: } else {
347: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
348: for ($i = 0; $i <= 31; $i++) {
349: $escape[] = chr($i);
350: }
351: $escape[] = chr(127);
352: }
353:
354: if ($hex == false) {
355: return $escape;
356: }
357: foreach ($escape as $key => $char) {
358: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
359: }
360: return $escape;
361: }
362:
363: 364: 365: 366: 367: 368:
369: public function offsetExists($offset) {
370: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
371: }
372:
373: 374: 375: 376: 377: 378:
379: public function offsetGet($offset) {
380: switch ($offset) {
381: case 'raw':
382: $firstLineLength = strpos($this->raw, "\r\n") + 2;
383: if ($this->raw[$firstLineLength] === "\r") {
384: $header = null;
385: } else {
386: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
387: }
388: return array(
389: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
390: 'header' => $header,
391: 'body' => $this->body,
392: 'response' => $this->raw
393: );
394: case 'status':
395: return array(
396: 'http-version' => $this->httpVersion,
397: 'code' => $this->code,
398: 'reason-phrase' => $this->reasonPhrase
399: );
400: case 'header':
401: return $this->headers;
402: case 'body':
403: return $this->body;
404: case 'cookies':
405: return $this->cookies;
406: }
407: return null;
408: }
409:
410: 411: 412: 413: 414: 415: 416:
417: public function offsetSet($offset, $value) {
418: }
419:
420: 421: 422: 423: 424: 425:
426: public function offsetUnset($offset) {
427: }
428:
429: 430: 431: 432: 433:
434: public function __toString() {
435: return $this->body();
436: }
437:
438: }
439: