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+(\w.+))?\s*\r\n/DU", $statusLine, $match)) {
163: $this->httpVersion = $match[1];
164: $this->code = $match[2];
165: if (isset($match[3])) {
166: $this->reasonPhrase = $match[3];
167: }
168: }
169:
170: $this->headers = $this->_parseHeader($header);
171: $transferEncoding = $this->getHeader('Transfer-Encoding');
172: $decoded = $this->_decodeBody($this->body, $transferEncoding);
173: $this->body = $decoded['body'];
174:
175: if (!empty($decoded['header'])) {
176: $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
177: }
178:
179: if (!empty($this->headers)) {
180: $this->cookies = $this->parseCookies($this->headers);
181: }
182: }
183:
184: 185: 186: 187: 188: 189: 190: 191:
192: protected function _decodeBody($body, $encoding = 'chunked') {
193: if (!is_string($body)) {
194: return false;
195: }
196: if (empty($encoding)) {
197: return array('body' => $body, 'header' => false);
198: }
199: $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
200:
201: if (!is_callable(array(&$this, $decodeMethod))) {
202: return array('body' => $body, 'header' => false);
203: }
204: return $this->{$decodeMethod}($body);
205: }
206:
207: 208: 209: 210: 211: 212: 213: 214:
215: protected function _decodeChunkedBody($body) {
216: if (!is_string($body)) {
217: return false;
218: }
219:
220: $decodedBody = null;
221: $chunkLength = null;
222:
223: while ($chunkLength !== 0) {
224: if (!preg_match('/^([0-9a-f]+)[ ]*(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
225:
226: preg_match('/^(.*?)\r\n/', $body, $invalidMatch);
227: $length = isset($invalidMatch[1]) ? strlen($invalidMatch[1]) : 0;
228: $match = array(
229: 0 => '',
230: 1 => dechex($length)
231: );
232: }
233: $chunkSize = 0;
234: $hexLength = 0;
235: if (isset($match[0])) {
236: $chunkSize = $match[0];
237: }
238: if (isset($match[1])) {
239: $hexLength = $match[1];
240: }
241:
242: $chunkLength = hexdec($hexLength);
243: $body = substr($body, strlen($chunkSize));
244:
245: $decodedBody .= substr($body, 0, $chunkLength);
246: if ($chunkLength) {
247: $body = substr($body, $chunkLength + strlen("\r\n"));
248: }
249: }
250:
251: $entityHeader = false;
252: if (!empty($body)) {
253: $entityHeader = $this->_parseHeader($body);
254: }
255: return array('body' => $decodedBody, 'header' => $entityHeader);
256: }
257:
258: 259: 260: 261: 262: 263:
264: protected function _parseHeader($header) {
265: if (is_array($header)) {
266: return $header;
267: } elseif (!is_string($header)) {
268: return false;
269: }
270:
271: preg_match_all("/(.+):(.+)(?:\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
272: $lines = explode("\r\n", $header);
273:
274: $header = array();
275: foreach ($lines as $line) {
276: if (strlen($line) === 0) {
277: continue;
278: }
279: $continuation = false;
280: $first = substr($line, 0, 1);
281:
282:
283: if ($first === ' ' || $first === "\t") {
284: $value .= preg_replace("/\s+/", ' ', $line);
285: $continuation = true;
286: } elseif (strpos($line, ':') !== false) {
287: list($field, $value) = explode(':', $line, 2);
288: $field = $this->_unescapeToken($field);
289: }
290:
291: $value = trim($value);
292: if (!isset($header[$field]) || $continuation) {
293: $header[$field] = $value;
294: } else {
295: $header[$field] = array_merge((array)$header[$field], (array)$value);
296: }
297: }
298: return $header;
299: }
300:
301: 302: 303: 304: 305: 306:
307: public function parseCookies($header) {
308: $cookieHeader = $this->getHeader('Set-Cookie', $header);
309: if (!$cookieHeader) {
310: return false;
311: }
312:
313: $cookies = array();
314: foreach ((array)$cookieHeader as $cookie) {
315: if (strpos($cookie, '";"') !== false) {
316: $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
317: $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
318: } else {
319: $parts = preg_split('/\;[ \t]*/', $cookie);
320: }
321:
322: list($name, $value) = explode('=', array_shift($parts), 2);
323: $cookies[$name] = compact('value');
324:
325: foreach ($parts as $part) {
326: if (strpos($part, '=') !== false) {
327: list($key, $value) = explode('=', $part);
328: } else {
329: $key = $part;
330: $value = true;
331: }
332:
333: $key = strtolower($key);
334: if (!isset($cookies[$name][$key])) {
335: $cookies[$name][$key] = $value;
336: }
337: }
338: }
339: return $cookies;
340: }
341:
342: 343: 344: 345: 346: 347: 348:
349: protected function _unescapeToken($token, $chars = null) {
350: $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
351: $token = preg_replace($regex, '\\1', $token);
352: return $token;
353: }
354:
355: 356: 357: 358: 359: 360: 361:
362: protected function _tokenEscapeChars($hex = true, $chars = null) {
363: if (!empty($chars)) {
364: $escape = $chars;
365: } else {
366: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
367: for ($i = 0; $i <= 31; $i++) {
368: $escape[] = chr($i);
369: }
370: $escape[] = chr(127);
371: }
372:
373: if (!$hex) {
374: return $escape;
375: }
376: foreach ($escape as $key => $char) {
377: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
378: }
379: return $escape;
380: }
381:
382: 383: 384: 385: 386: 387:
388: public function offsetExists($offset) {
389: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
390: }
391:
392: 393: 394: 395: 396: 397:
398: public function offsetGet($offset) {
399: switch ($offset) {
400: case 'raw':
401: $firstLineLength = strpos($this->raw, "\r\n") + 2;
402: if ($this->raw[$firstLineLength] === "\r") {
403: $header = null;
404: } else {
405: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
406: }
407: return array(
408: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
409: 'header' => $header,
410: 'body' => $this->body,
411: 'response' => $this->raw
412: );
413: case 'status':
414: return array(
415: 'http-version' => $this->httpVersion,
416: 'code' => $this->code,
417: 'reason-phrase' => $this->reasonPhrase
418: );
419: case 'header':
420: return $this->headers;
421: case 'body':
422: return $this->body;
423: case 'cookies':
424: return $this->cookies;
425: }
426: return null;
427: }
428:
429: 430: 431: 432: 433: 434: 435:
436: public function offsetSet($offset, $value) {
437: }
438:
439: 440: 441: 442: 443: 444:
445: public function offsetUnset($offset) {
446: }
447:
448: 449: 450: 451: 452:
453: public function __toString() {
454: return $this->body();
455: }
456:
457: }
458: