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 $this->code == 200;
125: }
126:
127: 128: 129: 130: 131: 132: 133:
134: public function parseResponse($message) {
135: if (!is_string($message)) {
136: throw new SocketException(__d('cake_dev', 'Invalid response.'));
137: }
138:
139: if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
140: throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
141: }
142:
143: list(, $statusLine, $header) = $match;
144: $this->raw = $message;
145: $this->body = (string)substr($message, strlen($match[0]));
146:
147: if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
148: $this->httpVersion = $match[1];
149: $this->code = $match[2];
150: $this->reasonPhrase = $match[3];
151: }
152:
153: $this->headers = $this->_parseHeader($header);
154: $transferEncoding = $this->getHeader('Transfer-Encoding');
155: $decoded = $this->_decodeBody($this->body, $transferEncoding);
156: $this->body = $decoded['body'];
157:
158: if (!empty($decoded['header'])) {
159: $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
160: }
161:
162: if (!empty($this->headers)) {
163: $this->cookies = $this->parseCookies($this->headers);
164: }
165: }
166:
167: 168: 169: 170: 171: 172: 173: 174:
175: protected function _decodeBody($body, $encoding = 'chunked') {
176: if (!is_string($body)) {
177: return false;
178: }
179: if (empty($encoding)) {
180: return array('body' => $body, 'header' => false);
181: }
182: $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
183:
184: if (!is_callable(array(&$this, $decodeMethod))) {
185: return array('body' => $body, 'header' => false);
186: }
187: return $this->{$decodeMethod}($body);
188: }
189:
190: 191: 192: 193: 194: 195: 196: 197:
198: protected function _decodeChunkedBody($body) {
199: if (!is_string($body)) {
200: return false;
201: }
202:
203: $decodedBody = null;
204: $chunkLength = null;
205:
206: while ($chunkLength !== 0) {
207: if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
208: throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
209: }
210:
211: $chunkSize = 0;
212: $hexLength = 0;
213: $chunkExtensionName = '';
214: $chunkExtensionValue = '';
215: if (isset($match[0])) {
216: $chunkSize = $match[0];
217: }
218: if (isset($match[1])) {
219: $hexLength = $match[1];
220: }
221: if (isset($match[2])) {
222: $chunkExtensionName = $match[2];
223: }
224: if (isset($match[3])) {
225: $chunkExtensionValue = $match[3];
226: }
227:
228: $body = substr($body, strlen($chunkSize));
229: $chunkLength = hexdec($hexLength);
230: $chunk = substr($body, 0, $chunkLength);
231: if (!empty($chunkExtensionName)) {
232: 233: 234:
235: }
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:
289: public function parseCookies($header) {
290: $cookieHeader = $this->getHeader('Set-Cookie', $header);
291: if (!$cookieHeader) {
292: return false;
293: }
294:
295: $cookies = array();
296: foreach ((array)$cookieHeader as $cookie) {
297: if (strpos($cookie, '";"') !== false) {
298: $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
299: $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
300: } else {
301: $parts = preg_split('/\;[ \t]*/', $cookie);
302: }
303:
304: list($name, $value) = explode('=', array_shift($parts), 2);
305: $cookies[$name] = compact('value');
306:
307: foreach ($parts as $part) {
308: if (strpos($part, '=') !== false) {
309: list($key, $value) = explode('=', $part);
310: } else {
311: $key = $part;
312: $value = true;
313: }
314:
315: $key = strtolower($key);
316: if (!isset($cookies[$name][$key])) {
317: $cookies[$name][$key] = $value;
318: }
319: }
320: }
321: return $cookies;
322: }
323:
324: 325: 326: 327: 328: 329: 330: 331:
332: protected function _unescapeToken($token, $chars = null) {
333: $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/';
334: $token = preg_replace($regex, '\\1', $token);
335: return $token;
336: }
337:
338: 339: 340: 341: 342: 343: 344: 345:
346: protected function _tokenEscapeChars($hex = true, $chars = null) {
347: if (!empty($chars)) {
348: $escape = $chars;
349: } else {
350: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
351: for ($i = 0; $i <= 31; $i++) {
352: $escape[] = chr($i);
353: }
354: $escape[] = chr(127);
355: }
356:
357: if ($hex == false) {
358: return $escape;
359: }
360: foreach ($escape as $key => $char) {
361: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
362: }
363: return $escape;
364: }
365:
366: 367: 368: 369: 370: 371:
372: public function offsetExists($offset) {
373: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
374: }
375:
376: 377: 378: 379: 380: 381:
382: public function offsetGet($offset) {
383: switch ($offset) {
384: case 'raw':
385: $firstLineLength = strpos($this->raw, "\r\n") + 2;
386: if ($this->raw[$firstLineLength] === "\r") {
387: $header = null;
388: } else {
389: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
390: }
391: return array(
392: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
393: 'header' => $header,
394: 'body' => $this->body,
395: 'response' => $this->raw
396: );
397: case 'status':
398: return array(
399: 'http-version' => $this->httpVersion,
400: 'code' => $this->code,
401: 'reason-phrase' => $this->reasonPhrase
402: );
403: case 'header':
404: return $this->headers;
405: case 'body':
406: return $this->body;
407: case 'cookies':
408: return $this->cookies;
409: }
410: return null;
411: }
412:
413: 414: 415: 416: 417: 418: 419:
420: public function offsetSet($offset, $value) {
421: return;
422: }
423:
424: 425: 426: 427: 428: 429:
430: public function offsetUnset($offset) {
431: return;
432: }
433:
434: 435: 436: 437: 438:
439: public function __toString() {
440: return $this->body();
441: }
442:
443: }
444: