~artur-barczynski/azsystem/trunk

« back to all changes in this revision

Viewing changes to lib/Cake/Network/Http/HttpResponse.php

  • Committer: Artur Barczynski
  • Date: 2012-09-20 16:31:07 UTC
  • Revision ID: artur@arturkb.pl-20120920163107-oakeg1a4h9e6d37f
Init

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/**
 
3
 * HTTP Response from HttpSocket.
 
4
 *
 
5
 * PHP 5
 
6
 *
 
7
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 
8
 * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
 
9
 *
 
10
 * Licensed under The MIT License
 
11
 * Redistributions of files must retain the above copyright notice.
 
12
 *
 
13
 * @copyright     Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
 
14
 * @link          http://cakephp.org CakePHP(tm) Project
 
15
 * @package       Cake.Network.Http
 
16
 * @since         CakePHP(tm) v 2.0.0
 
17
 * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
 
18
 */
 
19
 
 
20
/**
 
21
 * HTTP Response from HttpSocket.
 
22
 *
 
23
 * @package       Cake.Network.Http
 
24
 */
 
25
class HttpResponse implements ArrayAccess {
 
26
 
 
27
/**
 
28
 * Body content
 
29
 *
 
30
 * @var string
 
31
 */
 
32
        public $body = '';
 
33
 
 
34
/**
 
35
 * Headers
 
36
 *
 
37
 * @var array
 
38
 */
 
39
        public $headers = array();
 
40
 
 
41
/**
 
42
 * Cookies
 
43
 *
 
44
 * @var array
 
45
 */
 
46
        public $cookies = array();
 
47
 
 
48
/**
 
49
 * HTTP version
 
50
 *
 
51
 * @var string
 
52
 */
 
53
        public $httpVersion = 'HTTP/1.1';
 
54
 
 
55
/**
 
56
 * Response code
 
57
 *
 
58
 * @var integer
 
59
 */
 
60
        public $code = 0;
 
61
 
 
62
/**
 
63
 * Reason phrase
 
64
 *
 
65
 * @var string
 
66
 */
 
67
        public $reasonPhrase = '';
 
68
 
 
69
/**
 
70
 * Pure raw content
 
71
 *
 
72
 * @var string
 
73
 */
 
74
        public $raw = '';
 
75
 
 
76
/**
 
77
 * Constructor
 
78
 *
 
79
 * @param string $message
 
80
 */
 
81
        public function __construct($message = null) {
 
82
                if ($message !== null) {
 
83
                        $this->parseResponse($message);
 
84
                }
 
85
        }
 
86
 
 
87
/**
 
88
 * Body content
 
89
 *
 
90
 * @return string
 
91
 */
 
92
        public function body() {
 
93
                return (string)$this->body;
 
94
        }
 
95
 
 
96
/**
 
97
 * Get header in case insensitive
 
98
 *
 
99
 * @param string $name Header name
 
100
 * @param array $headers
 
101
 * @return mixed String if header exists or null
 
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
 * If return is 200 (OK)
 
120
 *
 
121
 * @return boolean
 
122
 */
 
123
        public function isOk() {
 
124
                return $this->code == 200;
 
125
        }
 
126
 
 
127
/**
 
128
 * If return is a valid 3xx (Redirection)
 
129
 *
 
130
 * @return boolean
 
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
 * Parses the given message and breaks it down in parts.
 
138
 *
 
139
 * @param string $message Message to parse
 
140
 * @return void
 
141
 * @throws SocketException
 
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
 * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
 
178
 * 'body' and 'header' or false on failure.
 
179
 *
 
180
 * @param string $body A string containing the body to decode.
 
181
 * @param string|boolean $encoding Can be false in case no encoding is being used, or a string representing the encoding.
 
182
 * @return mixed Array of response headers and body or false.
 
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
 * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
 
201
 * a result.
 
202
 *
 
203
 * @param string $body A string containing the chunked body to decode.
 
204
 * @return mixed Array of response headers and body or false.
 
205
 * @throws SocketException
 
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
                                 // @todo See if there are popular chunk extensions we should implement
 
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
 * Parses an array based header.
 
258
 *
 
259
 * @param array $header Header as an indexed array (field => value)
 
260
 * @return array Parsed header
 
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
 * Parses cookies in response headers.
 
291
 *
 
292
 * @param array $header Header array containing one ore more 'Set-Cookie' headers.
 
293
 * @return mixed Either false on no cookies, or an array of cookies received.
 
294
 * @todo Make this 100% RFC 2965 confirm
 
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
 * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
 
333
 *
 
334
 * @param string $token Token to unescape
 
335
 * @param array $chars
 
336
 * @return string Unescaped token
 
337
 * @todo Test $chars parameter
 
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
 * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
 
347
 *
 
348
 * @param boolean $hex true to get them as HEX values, false otherwise
 
349
 * @param array $chars
 
350
 * @return array Escape chars
 
351
 * @todo Test $chars parameter
 
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
 * ArrayAccess - Offset Exists
 
375
 *
 
376
 * @param string $offset
 
377
 * @return boolean
 
378
 */
 
379
        public function offsetExists($offset) {
 
380
                return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies'));
 
381
        }
 
382
 
 
383
/**
 
384
 * ArrayAccess - Offset Get
 
385
 *
 
386
 * @param string $offset
 
387
 * @return mixed
 
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
 * ArrayAccess - Offset Set
 
422
 *
 
423
 * @param string $offset
 
424
 * @param mixed $value
 
425
 * @return void
 
426
 */
 
427
        public function offsetSet($offset, $value) {
 
428
        }
 
429
 
 
430
/**
 
431
 * ArrayAccess - Offset Unset
 
432
 *
 
433
 * @param string $offset
 
434
 * @return void
 
435
 */
 
436
        public function offsetUnset($offset) {
 
437
        }
 
438
 
 
439
/**
 
440
 * Instance as string
 
441
 *
 
442
 * @return string
 
443
 */
 
444
        public function __toString() {
 
445
                return $this->body();
 
446
        }
 
447
 
 
448
}