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("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
272:
273: $header = array();
274: foreach ($matches as $match) {
275: list(, $field, $value) = $match;
276:
277: $value = trim($value);
278: $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
279:
280: $field = $this->_unescapeToken($field);
281:
282: if (!isset($header[$field])) {
283: $header[$field] = $value;
284: } else {
285: $header[$field] = array_merge((array)$header[$field], (array)$value);
286: }
287: }
288: return $header;
289: }
290:
291: 292: 293: 294: 295: 296:
297: public function parseCookies($header) {
298: $cookieHeader = $this->getHeader('Set-Cookie', $header);
299: if (!$cookieHeader) {
300: return false;
301: }
302:
303: $cookies = array();
304: foreach ((array)$cookieHeader as $cookie) {
305: if (strpos($cookie, '";"') !== false) {
306: $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
307: $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
308: } else {
309: $parts = preg_split('/\;[ \t]*/', $cookie);
310: }
311:
312: list($name, $value) = explode('=', array_shift($parts), 2);
313: $cookies[$name] = compact('value');
314:
315: foreach ($parts as $part) {
316: if (strpos($part, '=') !== false) {
317: list($key, $value) = explode('=', $part);
318: } else {
319: $key = $part;
320: $value = true;
321: }
322:
323: $key = strtolower($key);
324: if (!isset($cookies[$name][$key])) {
325: $cookies[$name][$key] = $value;
326: }
327: }
328: }
329: return $cookies;
330: }
331:
332: 333: 334: 335: 336: 337: 338:
339: protected function _unescapeToken($token, $chars = null) {
340: $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
341: $token = preg_replace($regex, '\\1', $token);
342: return $token;
343: }
344:
345: 346: 347: 348: 349: 350: 351:
352: protected function _tokenEscapeChars($hex = true, $chars = null) {
353: if (!empty($chars)) {
354: $escape = $chars;
355: } else {
356: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
357: for ($i = 0; $i <= 31; $i++) {
358: $escape[] = chr($i);
359: }
360: $escape[] = chr(127);
361: }
362:
363: if (!$hex) {
364: return $escape;
365: }
366: foreach ($escape as $key => $char) {
367: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
368: }
369: return $escape;
370: }
371:
372: 373: 374: 375: 376: 377:
378: public function offsetExists($offset) {
379: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
380: }
381:
382: 383: 384: 385: 386: 387:
388: public function offsetGet($offset) {
389: switch ($offset) {
390: case 'raw':
391: $firstLineLength = strpos($this->raw, "\r\n") + 2;
392: if ($this->raw[$firstLineLength] === "\r") {
393: $header = null;
394: } else {
395: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
396: }
397: return array(
398: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
399: 'header' => $header,
400: 'body' => $this->body,
401: 'response' => $this->raw
402: );
403: case 'status':
404: return array(
405: 'http-version' => $this->httpVersion,
406: 'code' => $this->code,
407: 'reason-phrase' => $this->reasonPhrase
408: );
409: case 'header':
410: return $this->headers;
411: case 'body':
412: return $this->body;
413: case 'cookies':
414: return $this->cookies;
415: }
416: return null;
417: }
418:
419: 420: 421: 422: 423: 424: 425:
426: public function offsetSet($offset, $value) {
427: }
428:
429: 430: 431: 432: 433: 434:
435: public function offsetUnset($offset) {
436: }
437:
438: 439: 440: 441: 442:
443: public function __toString() {
444: return $this->body();
445: }
446:
447: }
448: