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