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