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: public function isRedirect() {
133: return in_array($this->code, array(301, 302, 303, 307)) && !is_null($this->getHeader('Location'));
134: }
135:
136: 137: 138: 139: 140: 141: 142:
143: public function parseResponse($message) {
144: if (!is_string($message)) {
145: throw new SocketException(__d('cake_dev', 'Invalid response.'));
146: }
147:
148: if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
149: throw new SocketException(__d('cake_dev', 'Invalid HTTP response.'));
150: }
151:
152: list(, $statusLine, $header) = $match;
153: $this->raw = $message;
154: $this->body = (string)substr($message, strlen($match[0]));
155:
156: if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $statusLine, $match)) {
157: $this->httpVersion = $match[1];
158: $this->code = $match[2];
159: $this->reasonPhrase = $match[3];
160: }
161:
162: $this->headers = $this->_parseHeader($header);
163: $transferEncoding = $this->getHeader('Transfer-Encoding');
164: $decoded = $this->_decodeBody($this->body, $transferEncoding);
165: $this->body = $decoded['body'];
166:
167: if (!empty($decoded['header'])) {
168: $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header']));
169: }
170:
171: if (!empty($this->headers)) {
172: $this->cookies = $this->parseCookies($this->headers);
173: }
174: }
175:
176: 177: 178: 179: 180: 181: 182: 183:
184: protected function _decodeBody($body, $encoding = 'chunked') {
185: if (!is_string($body)) {
186: return false;
187: }
188: if (empty($encoding)) {
189: return array('body' => $body, 'header' => false);
190: }
191: $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body';
192:
193: if (!is_callable(array(&$this, $decodeMethod))) {
194: return array('body' => $body, 'header' => false);
195: }
196: return $this->{$decodeMethod}($body);
197: }
198:
199: 200: 201: 202: 203: 204: 205: 206:
207: protected function _decodeChunkedBody($body) {
208: if (!is_string($body)) {
209: return false;
210: }
211:
212: $decodedBody = null;
213: $chunkLength = null;
214:
215: while ($chunkLength !== 0) {
216: if (!preg_match('/^([0-9a-f]+) *(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) {
217: throw new SocketException(__d('cake_dev', 'HttpSocket::_decodeChunkedBody - Could not parse malformed chunk.'));
218: }
219:
220: $chunkSize = 0;
221: $hexLength = 0;
222: $chunkExtensionName = '';
223: $chunkExtensionValue = '';
224: if (isset($match[0])) {
225: $chunkSize = $match[0];
226: }
227: if (isset($match[1])) {
228: $hexLength = $match[1];
229: }
230: if (isset($match[2])) {
231: $chunkExtensionName = $match[2];
232: }
233: if (isset($match[3])) {
234: $chunkExtensionValue = $match[3];
235: }
236:
237: $body = substr($body, strlen($chunkSize));
238: $chunkLength = hexdec($hexLength);
239: $chunk = substr($body, 0, $chunkLength);
240: if (!empty($chunkExtensionName)) {
241:
242: }
243: $decodedBody .= $chunk;
244: if ($chunkLength !== 0) {
245: $body = substr($body, $chunkLength + strlen("\r\n"));
246: }
247: }
248:
249: $entityHeader = false;
250: if (!empty($body)) {
251: $entityHeader = $this->_parseHeader($body);
252: }
253: return array('body' => $decodedBody, 'header' => $entityHeader);
254: }
255:
256: 257: 258: 259: 260: 261:
262: protected function _parseHeader($header) {
263: if (is_array($header)) {
264: return $header;
265: } elseif (!is_string($header)) {
266: return false;
267: }
268:
269: preg_match_all("/(.+):(.+)(?:(?<![\t ])\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER);
270:
271: $header = array();
272: foreach ($matches as $match) {
273: list(, $field, $value) = $match;
274:
275: $value = trim($value);
276: $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
277:
278: $field = $this->_unescapeToken($field);
279:
280: if (!isset($header[$field])) {
281: $header[$field] = $value;
282: } else {
283: $header[$field] = array_merge((array)$header[$field], (array)$value);
284: }
285: }
286: return $header;
287: }
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:
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:
353: protected function _tokenEscapeChars($hex = true, $chars = null) {
354: if (!empty($chars)) {
355: $escape = $chars;
356: } else {
357: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
358: for ($i = 0; $i <= 31; $i++) {
359: $escape[] = chr($i);
360: }
361: $escape[] = chr(127);
362: }
363:
364: if ($hex == false) {
365: return $escape;
366: }
367: foreach ($escape as $key => $char) {
368: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
369: }
370: return $escape;
371: }
372:
373: 374: 375: 376: 377: 378:
379: public function offsetExists($offset) {
380: return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
381: }
382:
383: 384: 385: 386: 387: 388:
389: public function offsetGet($offset) {
390: switch ($offset) {
391: case 'raw':
392: $firstLineLength = strpos($this->raw, "\r\n") + 2;
393: if ($this->raw[$firstLineLength] === "\r") {
394: $header = null;
395: } else {
396: $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n";
397: }
398: return array(
399: 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n",
400: 'header' => $header,
401: 'body' => $this->body,
402: 'response' => $this->raw
403: );
404: case 'status':
405: return array(
406: 'http-version' => $this->httpVersion,
407: 'code' => $this->code,
408: 'reason-phrase' => $this->reasonPhrase
409: );
410: case 'header':
411: return $this->headers;
412: case 'body':
413: return $this->body;
414: case 'cookies':
415: return $this->cookies;
416: }
417: return null;
418: }
419:
420: 421: 422: 423: 424: 425: 426:
427: public function offsetSet($offset, $value) {
428: }
429:
430: 431: 432: 433: 434: 435:
436: public function offsetUnset($offset) {
437: }
438:
439: 440: 441: 442: 443:
444: public function __toString() {
445: return $this->body();
446: }
447:
448: }
449: