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 HttpSocketResponse 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:
82: public $context = array();
83:
84: 85: 86: 87: 88:
89: public function __construct($message = null) {
90: if ($message !== null) {
91: $this->parseResponse($message);
92: }
93: }
94:
95: 96: 97: 98: 99:
100: public function body() {
101: return (string)$this->body;
102: }
103:
104: 105: 106: 107: 108: 109: 110:
111: public function getHeader($name, $headers = null) {
112: if (!is_array($headers)) {
113: $headers =& $this->headers;
114: }
115: if (isset($headers[$name])) {
116: return $headers[$name];
117: }
118: foreach ($headers as $key => $value) {
119: if (strcasecmp($key, $name) === 0) {
120: return $value;
121: }
122: }
123: return null;
124: }
125:
126: 127: 128: 129: 130:
131: public function isOk() {
132: return in_array($this->code, array(200, 201, 202, 203, 204, 205, 206));
133: }
134:
135: 136: 137: 138: 139:
140: public function isRedirect() {
141: return in_array($this->code, array(301, 302, 303, 307)) && $this->getHeader('Location') !== null;
142: }
143:
144: 145: 146: 147: 148: 149: 150:
151: public function parseResponse($message) {
152: if (!is_string($message)) {
153: throw new SocketException(__d('cake_dev', 'Invalid response.'));
154: }
155:
156: if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
157: throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
158: }
159:
160: list(, $statusLine, $header) = $match;
161: $this->raw = $message;
162: $this->body = (string)substr($message, strlen($match[0]));
163:
164: if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
165: $this->httpVersion = $match[1];
166: $this->code = $match[2];
167: $this->reasonPhrase = $match[3];
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: throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
226: }
227:
228: $chunkSize = 0;
229: $hexLength = 0;
230: $chunkExtensionValue = '';
231: if (isset($match[0])) {
232: $chunkSize = $match[0];
233: }
234: if (isset($match[1])) {
235: $hexLength = $match[1];
236: }
237: if (isset($match[3])) {
238: $chunkExtensionValue = $match[3];
239: }
240:
241: $body = substr($body, strlen($chunkSize));
242: $chunkLength = hexdec($hexLength);
243: $chunk = substr($body, 0, $chunkLength);
244: $decodedBody .= $chunk;
245: if ($chunkLength !== 0) {
246: $body = substr($body, $chunkLength + strlen("\r\n"));
247: }
248: }
249:
250: $entityHeader = false;
251: if (!empty($body)) {
252: $entityHeader = $this->_parseHeader($body);
253: }
254: return array('body' => $decodedBody, 'header' => $entityHeader);
255: }
256:
257: 258: 259: 260: 261: 262:
263: protected function _parseHeader($header) {
264: if (is_array($header)) {
265: return $header;
266: } elseif (!is_string($header)) {
267: return false;
268: }
269:
270: preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
271:
272: $header = array();
273: foreach ($matches as $match) {
274: list(, $field, $value) = $match;
275:
276: $value = trim($value);
277: $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
278:
279: $field = $this->_unescapeToken($field);
280:
281: if (!isset($header[$field])) {
282: $header[$field] = $value;
283: } else {
284: $header[$field] = array_merge((array)$header[$field], (array)$value);
285: }
286: }
287: return $header;
288: }
289:
290: 291: 292: 293: 294: 295:
296: public function parseCookies($header) {
297: $cookieHeader = $this->getHeader('Set-Cookie', $header);
298: if (!$cookieHeader) {
299: return false;
300: }
301:
302: $cookies = array();
303: foreach ((array)$cookieHeader as $cookie) {
304: if (strpos($cookie, '";"') !== false) {
305: $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
306: $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
307: } else {
308: $parts = preg_split('/\;[ \t]*/', $cookie);
309: }
310:
311: list($name, $value) = explode('=', array_shift($parts), 2);
312: $cookies[$name] = compact('value');
313:
314: foreach ($parts as $part) {
315: if (strpos($part, '=') !== false) {
316: list($key, $value) = explode('=', $part);
317: } else {
318: $key = $part;
319: $value = true;
320: }
321:
322: $key = strtolower($key);
323: if (!isset($cookies[$name][$key])) {
324: $cookies[$name][$key] = $value;
325: }
326: }
327: }
328: return $cookies;
329: }
330:
331: 332: 333: 334: 335: 336: 337:
338: protected function _unescapeToken($token, $chars = null) {
339: $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
340: $token = preg_replace($regex, '\\1', $token);
341: return $token;
342: }
343:
344: 345: 346: 347: 348: 349: 350:
351: protected function _tokenEscapeChars($hex = true, $chars = null) {
352: if (!empty($chars)) {
353: $escape = $chars;
354: } else {
355: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
356: for ($i = 0; $i <= 31; $i++) {
357: $escape[] = chr($i);
358: }
359: $escape[] = chr(127);
360: }
361:
362: if (!$hex) {
363: return $escape;
364: }
365: foreach ($escape as $key => $char) {
366: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
367: }
368: return $escape;
369: }
370:
371: 372: 373: 374: 375: 376:
377: public function offsetExists($offset) {
378: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
379: }
380:
381: 382: 383: 384: 385: 386:
387: public function offsetGet($offset) {
388: switch ($offset) {
389: case 'raw':
390: $firstLineLength = strpos($this->raw, "\r\n") + 2;
391: if ($this->raw[$firstLineLength] === "\r") {
392: $header = null;
393: } else {
394: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
395: }
396: return array(
397: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
398: 'header' => $header,
399: 'body' => $this->body,
400: 'response' => $this->raw
401: );
402: case 'status':
403: return array(
404: 'http-version' => $this->httpVersion,
405: 'code' => $this->code,
406: 'reason-phrase' => $this->reasonPhrase
407: );
408: case 'header':
409: return $this->headers;
410: case 'body':
411: return $this->body;
412: case 'cookies':
413: return $this->cookies;
414: }
415: return null;
416: }
417:
418: 419: 420: 421: 422: 423: 424:
425: public function offsetSet($offset, $value) {
426: }
427:
428: 429: 430: 431: 432: 433:
434: public function offsetUnset($offset) {
435: }
436:
437: 438: 439: 440: 441:
442: public function __toString() {
443: return $this->body();
444: }
445:
446: }
447: