source: trunk/www.guidonia.net/wp/wp-content/plugins/tubepress/classes/net/php/pear/HTTP/Request2/Response.class.php@ 44

Last change on this file since 44 was 44, checked in by luciano, 14 years ago
File size: 17.7 KB
Line 
1<?php
2/**
3 * Class representing a HTTP response
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2008, Alexey Borzov <avb@php.net>
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 * * Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 * * Redistributions in binary form must reproduce the above copyright
19 * notice, this list of conditions and the following disclaimer in the
20 * documentation and/or other materials provided with the distribution.
21 * * The names of the authors may not be used to endorse or promote products
22 * derived from this software without specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
25 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
26 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
27 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
31 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
32 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
33 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 *
36 * @category HTTP
37 * @package HTTP_Request2
38 * @author Alexey Borzov <avb@php.net>
39 * @license http://opensource.org/licenses/bsd-license.php New BSD License
40 * @version CVS: $Id: Response.php,v 1.2 2008/11/15 10:59:30 avb Exp $
41 * @link http://pear.php.net/package/HTTP_Request2
42 */
43
44/**
45 * Exception class for HTTP_Request2 package
46 */
47
48/**
49 * Class representing a HTTP response
50 *
51 * The class is designed to be used in "streaming" scenario, building the
52 * response as it is being received:
53 * <code>
54 * $statusLine = read_status_line();
55 * $response = new HTTP_Request2_Response($statusLine);
56 * do {
57 * $headerLine = read_header_line();
58 * $response->parseHeaderLine($headerLine);
59 * } while ($headerLine != '');
60 *
61 * while ($chunk = read_body()) {
62 * $response->appendBody($chunk);
63 * }
64 *
65 * var_dump($response->getHeader(), $response->getCookies(), $response->getBody());
66 * </code>
67 *
68 *
69 * @category HTTP
70 * @package HTTP_Request2
71 * @author Alexey Borzov <avb@php.net>
72 * @version Release: 0.1.0
73 * @link http://tools.ietf.org/html/rfc2616#section-6
74 */
75class net_php_pear_HTTP_Request2_Response
76{
77 /**
78 * HTTP protocol version (e.g. 1.0, 1.1)
79 * @var string
80 */
81 protected $version;
82
83 /**
84 * Status code
85 * @var integer
86 * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
87 */
88 protected $code;
89
90 /**
91 * Reason phrase
92 * @var string
93 * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
94 */
95 protected $reasonPhrase;
96
97 /**
98 * Associative array of response headers
99 * @var array
100 */
101 protected $headers = array();
102
103 /**
104 * Cookies set in the response
105 * @var array
106 */
107 protected $cookies = array();
108
109 /**
110 * Name of last header processed by parseHederLine()
111 *
112 * Used to handle the headers that span multiple lines
113 *
114 * @var string
115 */
116 protected $lastHeader = null;
117
118 /**
119 * Response body
120 * @var string
121 */
122 protected $body = '';
123
124 /**
125 * Whether the body is still encoded by Content-Encoding
126 *
127 * cURL provides the decoded body to the callback; if we are reading from
128 * socket the body is still gzipped / deflated
129 *
130 * @var bool
131 */
132 protected $bodyEncoded;
133
134 /**
135 * Associative array of HTTP status code / reason phrase.
136 *
137 * @var array
138 * @link http://tools.ietf.org/html/rfc2616#section-10
139 */
140 protected static $phrases = array(
141
142 // 1xx: Informational - Request received, continuing process
143 100 => 'Continue',
144 101 => 'Switching Protocols',
145
146 // 2xx: Success - The action was successfully received, understood and
147 // accepted
148 200 => 'OK',
149 201 => 'Created',
150 202 => 'Accepted',
151 203 => 'Non-Authoritative Information',
152 204 => 'No Content',
153 205 => 'Reset Content',
154 206 => 'Partial Content',
155
156 // 3xx: Redirection - Further action must be taken in order to complete
157 // the request
158 300 => 'Multiple Choices',
159 301 => 'Moved Permanently',
160 302 => 'Found', // 1.1
161 303 => 'See Other',
162 304 => 'Not Modified',
163 305 => 'Use Proxy',
164 307 => 'Temporary Redirect',
165
166 // 4xx: Client Error - The request contains bad syntax or cannot be
167 // fulfilled
168 400 => 'Bad Request',
169 401 => 'Unauthorized',
170 402 => 'Payment Required',
171 403 => 'Forbidden',
172 404 => 'Not Found',
173 405 => 'Method Not Allowed',
174 406 => 'Not Acceptable',
175 407 => 'Proxy Authentication Required',
176 408 => 'Request Timeout',
177 409 => 'Conflict',
178 410 => 'Gone',
179 411 => 'Length Required',
180 412 => 'Precondition Failed',
181 413 => 'Request Entity Too Large',
182 414 => 'Request-URI Too Long',
183 415 => 'Unsupported Media Type',
184 416 => 'Requested Range Not Satisfiable',
185 417 => 'Expectation Failed',
186
187 // 5xx: Server Error - The server failed to fulfill an apparently
188 // valid request
189 500 => 'Internal Server Error',
190 501 => 'Not Implemented',
191 502 => 'Bad Gateway',
192 503 => 'Service Unavailable',
193 504 => 'Gateway Timeout',
194 505 => 'HTTP Version Not Supported',
195 509 => 'Bandwidth Limit Exceeded',
196
197 );
198
199 /**
200 * Constructor, parses the response status line
201 *
202 * @param string Response status line (e.g. "HTTP/1.1 200 OK")
203 * @param bool Whether body is still encoded by Content-Encoding
204 * @throws net_php_pear_HTTP_Request2_Exception if status line is invalid according to spec
205 */
206 public function __construct($statusLine, $bodyEncoded = true)
207 {
208 if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {
209 throw new net_php_pear_HTTP_Request2_Exception("Malformed response: {$statusLine}");
210 }
211 $this->version = $m[1];
212 $this->code = intval($m[2]);
213 if (!empty($m[3])) {
214 $this->reasonPhrase = trim($m[3]);
215 } elseif (!empty(self::$phrases[$this->code])) {
216 $this->reasonPhrase = self::$phrases[$this->code];
217 }
218 $this->bodyEncoded = (bool)$bodyEncoded;
219 }
220
221 /**
222 * Parses the line from HTTP response filling $headers array
223 *
224 * The method should be called after reading the line from socket or receiving
225 * it into cURL callback. Passing an empty string here indicates the end of
226 * response headers and triggers additional processing, so be sure to pass an
227 * empty string in the end.
228 *
229 * @param string Line from HTTP response
230 */
231 public function parseHeaderLine($headerLine)
232 {
233 $headerLine = trim($headerLine, "\r\n");
234
235 // empty string signals the end of headers, process the received ones
236 if ('' == $headerLine) {
237 if (!empty($this->headers['set-cookie'])) {
238 $cookies = is_array($this->headers['set-cookie'])?
239 $this->headers['set-cookie']:
240 array($this->headers['set-cookie']);
241 foreach ($cookies as $cookieString) {
242 $this->parseCookie($cookieString);
243 }
244 unset($this->headers['set-cookie']);
245 }
246 foreach (array_keys($this->headers) as $k) {
247 if (is_array($this->headers[$k])) {
248 $this->headers[$k] = implode(', ', $this->headers[$k]);
249 }
250 }
251
252 // string of the form header-name: header value
253 } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {
254 $name = strtolower($m[1]);
255 $value = trim($m[2]);
256 if (empty($this->headers[$name])) {
257 $this->headers[$name] = $value;
258 } else {
259 if (!is_array($this->headers[$name])) {
260 $this->headers[$name] = array($this->headers[$name]);
261 }
262 $this->headers[$name][] = $value;
263 }
264 $this->lastHeader = $name;
265
266 // string
267 } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {
268 if (!is_array($this->headers[$this->lastHeader])) {
269 $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);
270 } else {
271 $key = count($this->headers[$this->lastHeader]) - 1;
272 $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);
273 }
274 }
275 }
276
277 /**
278 * Parses a Set-Cookie header to fill $cookies array
279 *
280 * @param string value of Set-Cookie header
281 * @link http://cgi.netscape.com/newsref/std/cookie_spec.html
282 */
283 protected function parseCookie($cookieString)
284 {
285 $cookie = array(
286 'expires' => null,
287 'domain' => null,
288 'path' => null,
289 'secure' => false
290 );
291
292 // Only a name=value pair
293 if (!strpos($cookieString, ';')) {
294 $pos = strpos($cookieString, '=');
295 $cookie['name'] = trim(substr($cookieString, 0, $pos));
296 $cookie['value'] = trim(substr($cookieString, $pos + 1));
297
298 // Some optional parameters are supplied
299 } else {
300 $elements = explode(';', $cookieString);
301 $pos = strpos($elements[0], '=');
302 $cookie['name'] = trim(substr($elements[0], 0, $pos));
303 $cookie['value'] = trim(substr($elements[0], $pos + 1));
304
305 for ($i = 1; $i < count($elements); $i++) {
306 if (false === strpos($elements[$i], '=')) {
307 $elName = trim($elements[$i]);
308 $elValue = null;
309 } else {
310 list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
311 }
312 $elName = strtolower($elName);
313 if ('secure' == $elName) {
314 $cookie['secure'] = true;
315 } elseif ('expires' == $elName) {
316 $cookie['expires'] = str_replace('"', '', $elValue);
317 } elseif ('path' == $elName || 'domain' == $elName) {
318 $cookie[$elName] = urldecode($elValue);
319 } else {
320 $cookie[$elName] = $elValue;
321 }
322 }
323 }
324 $this->cookies[] = $cookie;
325 }
326
327 /**
328 * Appends a string to the response body
329 * @param string
330 */
331 public function appendBody($bodyChunk)
332 {
333 $this->body .= $bodyChunk;
334 }
335
336 /**
337 * Returns the status code
338 * @return integer
339 */
340 public function getStatus()
341 {
342 return $this->code;
343 }
344
345 /**
346 * Returns the reason phrase
347 * @return string
348 */
349 public function getReasonPhrase()
350 {
351 return $this->reasonPhrase;
352 }
353
354 /**
355 * Returns either the named header or all response headers
356 *
357 * @param string Name of header to return
358 * @return string|array Value of $headerName header (null if header is
359 * not present), array of all response headers if
360 * $headerName is null
361 */
362 public function getHeader($headerName = null)
363 {
364 if (null === $headerName) {
365 return $this->headers;
366 } else {
367 $headerName = strtolower($headerName);
368 return isset($this->headers[$headerName])? $this->headers[$headerName]: null;
369 }
370 }
371
372 /**
373 * Returns cookies set in response
374 *
375 * @return array
376 */
377 public function getCookies()
378 {
379 return $this->cookies;
380 }
381
382 /**
383 * Returns the body of the response
384 *
385 * @return string
386 * @throws net_php_pear_HTTP_Request2_Exception if body cannot be decoded
387 */
388 public function getBody()
389 {
390 if ($this->bodyEncoded) {
391 switch (strtolower($this->getHeader('content-encoding'))) {
392 case 'gzip':
393 return self::decodeGzip($this->body);
394 case 'deflate':
395 return self::decodeDeflate($this->body);
396 }
397 }
398 return $this->body;
399 }
400
401 /**
402 * Get the HTTP version of the response
403 *
404 * @return string
405 */
406 public function getVersion()
407 {
408 return $this->version;
409 }
410
411 /**
412 * Decodes the message-body encoded by gzip
413 *
414 * The real decoding work is done by gzinflate() built-in function, this
415 * method only parses the header and checks data for compliance with
416 * RFC 1952
417 *
418 * @param string gzip-encoded data
419 * @return string decoded data
420 * @throws net_php_pear_HTTP_Request2_Exception
421 * @link http://tools.ietf.org/html/rfc1952
422 */
423 public static function decodeGzip($data)
424 {
425 $length = strlen($data);
426 // If it doesn't look like gzip-encoded data, don't bother
427 if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
428 return $data;
429 }
430 if (!function_exists('gzinflate')) {
431 throw new net_php_pear_HTTP_Request2_Exception('Unable to decode body: gzip extension not available');
432 }
433 $method = ord(substr($data, 2, 1));
434 if (8 != $method) {
435 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: unknown compression method');
436 }
437 $flags = ord(substr($data, 3, 1));
438 if ($flags & 224) {
439 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: reserved bits are set');
440 }
441
442 // header is 10 bytes minimum. may be longer, though.
443 $headerLength = 10;
444 // extra fields, need to skip 'em
445 if ($flags & 4) {
446 if ($length - $headerLength - 2 < 8) {
447 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
448 }
449 $extraLength = unpack('v', substr($data, 10, 2));
450 if ($length - $headerLength - 2 - $extraLength[1] < 8) {
451 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
452 }
453 $headerLength += $extraLength[1] + 2;
454 }
455 // file name, need to skip that
456 if ($flags & 8) {
457 if ($length - $headerLength - 1 < 8) {
458 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
459 }
460 $filenameLength = strpos(substr($data, $headerLength), chr(0));
461 if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
462 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
463 }
464 $headerLength += $filenameLength + 1;
465 }
466 // comment, need to skip that also
467 if ($flags & 16) {
468 if ($length - $headerLength - 1 < 8) {
469 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
470 }
471 $commentLength = strpos(substr($data, $headerLength), chr(0));
472 if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
473 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
474 }
475 $headerLength += $commentLength + 1;
476 }
477 // have a CRC for header. let's check
478 if ($flags & 2) {
479 if ($length - $headerLength - 2 < 8) {
480 throw new net_php_pear_HTTP_Request2_Exception('Error parsing gzip header: data too short');
481 }
482 $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));
483 $crcStored = unpack('v', substr($data, $headerLength, 2));
484 if ($crcReal != $crcStored[1]) {
485 throw new net_php_pear_HTTP_Request2_Exception('Header CRC check failed');
486 }
487 $headerLength += 2;
488 }
489 // unpacked data CRC and size at the end of encoded data
490 $tmp = unpack('V2', substr($data, -8));
491 $dataCrc = $tmp[1];
492 $dataSize = $tmp[2];
493
494 // finally, call the gzinflate() function
495 // don't pass $dataSize to gzinflate, see bugs #13135, #14370
496 $unpacked = gzinflate(substr($data, $headerLength, -8));
497 if (false === $unpacked) {
498 throw new net_php_pear_HTTP_Request2_Exception('gzinflate() call failed');
499 } elseif ($dataSize != strlen($unpacked)) {
500 throw new net_php_pear_HTTP_Request2_Exception('Data size check failed');
501 } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
502 throw new net_php_pear_HTTP_Request2_Exception('Data CRC check failed');
503 }
504 return $unpacked;
505 }
506
507 /**
508 * Decodes the message-body encoded by deflate
509 *
510 * @param string deflate-encoded data
511 * @return string decoded data
512 * @throws net_php_pear_HTTP_Request2_Exception
513 */
514 public static function decodeDeflate($data)
515 {
516 if (!function_exists('gzuncompress')) {
517 throw new net_php_pear_HTTP_Request2_Exception('Unable to decode body: gzip extension not available');
518 }
519 return gzuncompress($data);
520 }
521}
522?>
Note: See TracBrowser for help on using the repository browser.