]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/HTTP/Request2/Response.php
neo-quitter theme from hannesmannerheim
[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  * This source file is subject to BSD 3-Clause License that is bundled\r
10  * with this package in the file LICENSE and available at the URL\r
11  * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE\r
12  *\r
13  * @category  HTTP\r
14  * @package   HTTP_Request2\r
15  * @author    Alexey Borzov <avb@php.net>\r
16  * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
17  * @license   http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
18  * @link      http://pear.php.net/package/HTTP_Request2\r
19  */\r
20 \r
21 /**\r
22  * Exception class for HTTP_Request2 package\r
23  */\r
24 require_once 'HTTP/Request2/Exception.php';\r
25 \r
26 /**\r
27  * Class representing a HTTP response\r
28  *\r
29  * The class is designed to be used in "streaming" scenario, building the\r
30  * response as it is being received:\r
31  * <code>\r
32  * $statusLine = read_status_line();\r
33  * $response = new HTTP_Request2_Response($statusLine);\r
34  * do {\r
35  *     $headerLine = read_header_line();\r
36  *     $response->parseHeaderLine($headerLine);\r
37  * } while ($headerLine != '');\r
38  *\r
39  * while ($chunk = read_body()) {\r
40  *     $response->appendBody($chunk);\r
41  * }\r
42  *\r
43  * var_dump($response->getHeader(), $response->getCookies(), $response->getBody());\r
44  * </code>\r
45  *\r
46  * @category HTTP\r
47  * @package  HTTP_Request2\r
48  * @author   Alexey Borzov <avb@php.net>\r
49  * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
50  * @version  Release: 2.2.1\r
51  * @link     http://pear.php.net/package/HTTP_Request2\r
52  * @link     http://tools.ietf.org/html/rfc2616#section-6\r
53  */\r
54 class HTTP_Request2_Response\r
55 {\r
56     /**\r
57      * HTTP protocol version (e.g. 1.0, 1.1)\r
58      * @var  string\r
59      */\r
60     protected $version;\r
61 \r
62     /**\r
63      * Status code\r
64      * @var  integer\r
65      * @link http://tools.ietf.org/html/rfc2616#section-6.1.1\r
66      */\r
67     protected $code;\r
68 \r
69     /**\r
70      * Reason phrase\r
71      * @var  string\r
72      * @link http://tools.ietf.org/html/rfc2616#section-6.1.1\r
73      */\r
74     protected $reasonPhrase;\r
75 \r
76     /**\r
77      * Effective URL (may be different from original request URL in case of redirects)\r
78      * @var  string\r
79      */\r
80     protected $effectiveUrl;\r
81 \r
82     /**\r
83      * Associative array of response headers\r
84      * @var  array\r
85      */\r
86     protected $headers = array();\r
87 \r
88     /**\r
89      * Cookies set in the response\r
90      * @var  array\r
91      */\r
92     protected $cookies = array();\r
93 \r
94     /**\r
95      * Name of last header processed by parseHederLine()\r
96      *\r
97      * Used to handle the headers that span multiple lines\r
98      *\r
99      * @var  string\r
100      */\r
101     protected $lastHeader = null;\r
102 \r
103     /**\r
104      * Response body\r
105      * @var  string\r
106      */\r
107     protected $body = '';\r
108 \r
109     /**\r
110      * Whether the body is still encoded by Content-Encoding\r
111      *\r
112      * cURL provides the decoded body to the callback; if we are reading from\r
113      * socket the body is still gzipped / deflated\r
114      *\r
115      * @var  bool\r
116      */\r
117     protected $bodyEncoded;\r
118 \r
119     /**\r
120      * Associative array of HTTP status code / reason phrase.\r
121      *\r
122      * @var  array\r
123      * @link http://tools.ietf.org/html/rfc2616#section-10\r
124      */\r
125     protected static $phrases = array(\r
126 \r
127         // 1xx: Informational - Request received, continuing process\r
128         100 => 'Continue',\r
129         101 => 'Switching Protocols',\r
130 \r
131         // 2xx: Success - The action was successfully received, understood and\r
132         // accepted\r
133         200 => 'OK',\r
134         201 => 'Created',\r
135         202 => 'Accepted',\r
136         203 => 'Non-Authoritative Information',\r
137         204 => 'No Content',\r
138         205 => 'Reset Content',\r
139         206 => 'Partial Content',\r
140 \r
141         // 3xx: Redirection - Further action must be taken in order to complete\r
142         // the request\r
143         300 => 'Multiple Choices',\r
144         301 => 'Moved Permanently',\r
145         302 => 'Found',  // 1.1\r
146         303 => 'See Other',\r
147         304 => 'Not Modified',\r
148         305 => 'Use Proxy',\r
149         307 => 'Temporary Redirect',\r
150 \r
151         // 4xx: Client Error - The request contains bad syntax or cannot be\r
152         // fulfilled\r
153         400 => 'Bad Request',\r
154         401 => 'Unauthorized',\r
155         402 => 'Payment Required',\r
156         403 => 'Forbidden',\r
157         404 => 'Not Found',\r
158         405 => 'Method Not Allowed',\r
159         406 => 'Not Acceptable',\r
160         407 => 'Proxy Authentication Required',\r
161         408 => 'Request Timeout',\r
162         409 => 'Conflict',\r
163         410 => 'Gone',\r
164         411 => 'Length Required',\r
165         412 => 'Precondition Failed',\r
166         413 => 'Request Entity Too Large',\r
167         414 => 'Request-URI Too Long',\r
168         415 => 'Unsupported Media Type',\r
169         416 => 'Requested Range Not Satisfiable',\r
170         417 => 'Expectation Failed',\r
171 \r
172         // 5xx: Server Error - The server failed to fulfill an apparently\r
173         // valid request\r
174         500 => 'Internal Server Error',\r
175         501 => 'Not Implemented',\r
176         502 => 'Bad Gateway',\r
177         503 => 'Service Unavailable',\r
178         504 => 'Gateway Timeout',\r
179         505 => 'HTTP Version Not Supported',\r
180         509 => 'Bandwidth Limit Exceeded',\r
181 \r
182     );\r
183 \r
184     /**\r
185      * Returns the default reason phrase for the given code or all reason phrases\r
186      *\r
187      * @param int $code Response code\r
188      *\r
189      * @return string|array|null Default reason phrase for $code if $code is given\r
190      *                           (null if no phrase is available), array of all\r
191      *                           reason phrases if $code is null\r
192      * @link   http://pear.php.net/bugs/18716\r
193      */\r
194     public static function getDefaultReasonPhrase($code = null)\r
195     {\r
196         if (null === $code) {\r
197             return self::$phrases;\r
198         } else {\r
199             return isset(self::$phrases[$code]) ? self::$phrases[$code] : null;\r
200         }\r
201     }\r
202 \r
203     /**\r
204      * Constructor, parses the response status line\r
205      *\r
206      * @param string $statusLine   Response status line (e.g. "HTTP/1.1 200 OK")\r
207      * @param bool   $bodyEncoded  Whether body is still encoded by Content-Encoding\r
208      * @param string $effectiveUrl Effective URL of the response\r
209      *\r
210      * @throws   HTTP_Request2_MessageException if status line is invalid according to spec\r
211      */\r
212     public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)\r
213     {\r
214         if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {\r
215             throw new HTTP_Request2_MessageException(\r
216                 "Malformed response: {$statusLine}",\r
217                 HTTP_Request2_Exception::MALFORMED_RESPONSE\r
218             );\r
219         }\r
220         $this->version      = $m[1];\r
221         $this->code         = intval($m[2]);\r
222         $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code);\r
223         $this->bodyEncoded  = (bool)$bodyEncoded;\r
224         $this->effectiveUrl = (string)$effectiveUrl;\r
225     }\r
226 \r
227     /**\r
228      * Parses the line from HTTP response filling $headers array\r
229      *\r
230      * The method should be called after reading the line from socket or receiving\r
231      * it into cURL callback. Passing an empty string here indicates the end of\r
232      * response headers and triggers additional processing, so be sure to pass an\r
233      * empty string in the end.\r
234      *\r
235      * @param string $headerLine Line from HTTP response\r
236      */\r
237     public function parseHeaderLine($headerLine)\r
238     {\r
239         $headerLine = trim($headerLine, "\r\n");\r
240 \r
241         if ('' == $headerLine) {\r
242             // empty string signals the end of headers, process the received ones\r
243             if (!empty($this->headers['set-cookie'])) {\r
244                 $cookies = is_array($this->headers['set-cookie'])?\r
245                            $this->headers['set-cookie']:\r
246                            array($this->headers['set-cookie']);\r
247                 foreach ($cookies as $cookieString) {\r
248                     $this->parseCookie($cookieString);\r
249                 }\r
250                 unset($this->headers['set-cookie']);\r
251             }\r
252             foreach (array_keys($this->headers) as $k) {\r
253                 if (is_array($this->headers[$k])) {\r
254                     $this->headers[$k] = implode(', ', $this->headers[$k]);\r
255                 }\r
256             }\r
257 \r
258         } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {\r
259             // string of the form header-name: header value\r
260             $name  = strtolower($m[1]);\r
261             $value = trim($m[2]);\r
262             if (empty($this->headers[$name])) {\r
263                 $this->headers[$name] = $value;\r
264             } else {\r
265                 if (!is_array($this->headers[$name])) {\r
266                     $this->headers[$name] = array($this->headers[$name]);\r
267                 }\r
268                 $this->headers[$name][] = $value;\r
269             }\r
270             $this->lastHeader = $name;\r
271 \r
272         } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {\r
273             // continuation of a previous header\r
274             if (!is_array($this->headers[$this->lastHeader])) {\r
275                 $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);\r
276             } else {\r
277                 $key = count($this->headers[$this->lastHeader]) - 1;\r
278                 $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);\r
279             }\r
280         }\r
281     }\r
282 \r
283     /**\r
284      * Parses a Set-Cookie header to fill $cookies array\r
285      *\r
286      * @param string $cookieString value of Set-Cookie header\r
287      *\r
288      * @link     http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html\r
289      */\r
290     protected function parseCookie($cookieString)\r
291     {\r
292         $cookie = array(\r
293             'expires' => null,\r
294             'domain'  => null,\r
295             'path'    => null,\r
296             'secure'  => false\r
297         );\r
298 \r
299         if (!strpos($cookieString, ';')) {\r
300             // Only a name=value pair\r
301             $pos = strpos($cookieString, '=');\r
302             $cookie['name']  = trim(substr($cookieString, 0, $pos));\r
303             $cookie['value'] = trim(substr($cookieString, $pos + 1));\r
304 \r
305         } else {\r
306             // Some optional parameters are supplied\r
307             $elements = explode(';', $cookieString);\r
308             $pos = strpos($elements[0], '=');\r
309             $cookie['name']  = trim(substr($elements[0], 0, $pos));\r
310             $cookie['value'] = trim(substr($elements[0], $pos + 1));\r
311 \r
312             for ($i = 1; $i < count($elements); $i++) {\r
313                 if (false === strpos($elements[$i], '=')) {\r
314                     $elName  = trim($elements[$i]);\r
315                     $elValue = null;\r
316                 } else {\r
317                     list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));\r
318                 }\r
319                 $elName = strtolower($elName);\r
320                 if ('secure' == $elName) {\r
321                     $cookie['secure'] = true;\r
322                 } elseif ('expires' == $elName) {\r
323                     $cookie['expires'] = str_replace('"', '', $elValue);\r
324                 } elseif ('path' == $elName || 'domain' == $elName) {\r
325                     $cookie[$elName] = urldecode($elValue);\r
326                 } else {\r
327                     $cookie[$elName] = $elValue;\r
328                 }\r
329             }\r
330         }\r
331         $this->cookies[] = $cookie;\r
332     }\r
333 \r
334     /**\r
335      * Appends a string to the response body\r
336      *\r
337      * @param string $bodyChunk part of response body\r
338      */\r
339     public function appendBody($bodyChunk)\r
340     {\r
341         $this->body .= $bodyChunk;\r
342     }\r
343 \r
344     /**\r
345      * Returns the effective URL of the response\r
346      *\r
347      * This may be different from the request URL if redirects were followed.\r
348      *\r
349      * @return string\r
350      * @link   http://pear.php.net/bugs/bug.php?id=18412\r
351      */\r
352     public function getEffectiveUrl()\r
353     {\r
354         return $this->effectiveUrl;\r
355     }\r
356 \r
357     /**\r
358      * Returns the status code\r
359      *\r
360      * @return   integer\r
361      */\r
362     public function getStatus()\r
363     {\r
364         return $this->code;\r
365     }\r
366 \r
367     /**\r
368      * Returns the reason phrase\r
369      *\r
370      * @return   string\r
371      */\r
372     public function getReasonPhrase()\r
373     {\r
374         return $this->reasonPhrase;\r
375     }\r
376 \r
377     /**\r
378      * Whether response is a redirect that can be automatically handled by HTTP_Request2\r
379      *\r
380      * @return   bool\r
381      */\r
382     public function isRedirect()\r
383     {\r
384         return in_array($this->code, array(300, 301, 302, 303, 307))\r
385                && isset($this->headers['location']);\r
386     }\r
387 \r
388     /**\r
389      * Returns either the named header or all response headers\r
390      *\r
391      * @param string $headerName Name of header to return\r
392      *\r
393      * @return   string|array    Value of $headerName header (null if header is\r
394      *                           not present), array of all response headers if\r
395      *                           $headerName is null\r
396      */\r
397     public function getHeader($headerName = null)\r
398     {\r
399         if (null === $headerName) {\r
400             return $this->headers;\r
401         } else {\r
402             $headerName = strtolower($headerName);\r
403             return isset($this->headers[$headerName])? $this->headers[$headerName]: null;\r
404         }\r
405     }\r
406 \r
407     /**\r
408      * Returns cookies set in response\r
409      *\r
410      * @return   array\r
411      */\r
412     public function getCookies()\r
413     {\r
414         return $this->cookies;\r
415     }\r
416 \r
417     /**\r
418      * Returns the body of the response\r
419      *\r
420      * @return   string\r
421      * @throws   HTTP_Request2_Exception if body cannot be decoded\r
422      */\r
423     public function getBody()\r
424     {\r
425         if (0 == strlen($this->body) || !$this->bodyEncoded\r
426             || !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))\r
427         ) {\r
428             return $this->body;\r
429 \r
430         } else {\r
431             if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {\r
432                 $oldEncoding = mb_internal_encoding();\r
433                 mb_internal_encoding('8bit');\r
434             }\r
435 \r
436             try {\r
437                 switch (strtolower($this->getHeader('content-encoding'))) {\r
438                 case 'gzip':\r
439                     $decoded = self::decodeGzip($this->body);\r
440                     break;\r
441                 case 'deflate':\r
442                     $decoded = self::decodeDeflate($this->body);\r
443                 }\r
444             } catch (Exception $e) {\r
445             }\r
446 \r
447             if (!empty($oldEncoding)) {\r
448                 mb_internal_encoding($oldEncoding);\r
449             }\r
450             if (!empty($e)) {\r
451                 throw $e;\r
452             }\r
453             return $decoded;\r
454         }\r
455     }\r
456 \r
457     /**\r
458      * Get the HTTP version of the response\r
459      *\r
460      * @return   string\r
461      */\r
462     public function getVersion()\r
463     {\r
464         return $this->version;\r
465     }\r
466 \r
467     /**\r
468      * Decodes the message-body encoded by gzip\r
469      *\r
470      * The real decoding work is done by gzinflate() built-in function, this\r
471      * method only parses the header and checks data for compliance with\r
472      * RFC 1952\r
473      *\r
474      * @param string $data gzip-encoded data\r
475      *\r
476      * @return   string  decoded data\r
477      * @throws   HTTP_Request2_LogicException\r
478      * @throws   HTTP_Request2_MessageException\r
479      * @link     http://tools.ietf.org/html/rfc1952\r
480      */\r
481     public static function decodeGzip($data)\r
482     {\r
483         $length = strlen($data);\r
484         // If it doesn't look like gzip-encoded data, don't bother\r
485         if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {\r
486             return $data;\r
487         }\r
488         if (!function_exists('gzinflate')) {\r
489             throw new HTTP_Request2_LogicException(\r
490                 'Unable to decode body: gzip extension not available',\r
491                 HTTP_Request2_Exception::MISCONFIGURATION\r
492             );\r
493         }\r
494         $method = ord(substr($data, 2, 1));\r
495         if (8 != $method) {\r
496             throw new HTTP_Request2_MessageException(\r
497                 'Error parsing gzip header: unknown compression method',\r
498                 HTTP_Request2_Exception::DECODE_ERROR\r
499             );\r
500         }\r
501         $flags = ord(substr($data, 3, 1));\r
502         if ($flags & 224) {\r
503             throw new HTTP_Request2_MessageException(\r
504                 'Error parsing gzip header: reserved bits are set',\r
505                 HTTP_Request2_Exception::DECODE_ERROR\r
506             );\r
507         }\r
508 \r
509         // header is 10 bytes minimum. may be longer, though.\r
510         $headerLength = 10;\r
511         // extra fields, need to skip 'em\r
512         if ($flags & 4) {\r
513             if ($length - $headerLength - 2 < 8) {\r
514                 throw new HTTP_Request2_MessageException(\r
515                     'Error parsing gzip header: data too short',\r
516                     HTTP_Request2_Exception::DECODE_ERROR\r
517                 );\r
518             }\r
519             $extraLength = unpack('v', substr($data, 10, 2));\r
520             if ($length - $headerLength - 2 - $extraLength[1] < 8) {\r
521                 throw new HTTP_Request2_MessageException(\r
522                     'Error parsing gzip header: data too short',\r
523                     HTTP_Request2_Exception::DECODE_ERROR\r
524                 );\r
525             }\r
526             $headerLength += $extraLength[1] + 2;\r
527         }\r
528         // file name, need to skip that\r
529         if ($flags & 8) {\r
530             if ($length - $headerLength - 1 < 8) {\r
531                 throw new HTTP_Request2_MessageException(\r
532                     'Error parsing gzip header: data too short',\r
533                     HTTP_Request2_Exception::DECODE_ERROR\r
534                 );\r
535             }\r
536             $filenameLength = strpos(substr($data, $headerLength), chr(0));\r
537             if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {\r
538                 throw new HTTP_Request2_MessageException(\r
539                     'Error parsing gzip header: data too short',\r
540                     HTTP_Request2_Exception::DECODE_ERROR\r
541                 );\r
542             }\r
543             $headerLength += $filenameLength + 1;\r
544         }\r
545         // comment, need to skip that also\r
546         if ($flags & 16) {\r
547             if ($length - $headerLength - 1 < 8) {\r
548                 throw new HTTP_Request2_MessageException(\r
549                     'Error parsing gzip header: data too short',\r
550                     HTTP_Request2_Exception::DECODE_ERROR\r
551                 );\r
552             }\r
553             $commentLength = strpos(substr($data, $headerLength), chr(0));\r
554             if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {\r
555                 throw new HTTP_Request2_MessageException(\r
556                     'Error parsing gzip header: data too short',\r
557                     HTTP_Request2_Exception::DECODE_ERROR\r
558                 );\r
559             }\r
560             $headerLength += $commentLength + 1;\r
561         }\r
562         // have a CRC for header. let's check\r
563         if ($flags & 2) {\r
564             if ($length - $headerLength - 2 < 8) {\r
565                 throw new HTTP_Request2_MessageException(\r
566                     'Error parsing gzip header: data too short',\r
567                     HTTP_Request2_Exception::DECODE_ERROR\r
568                 );\r
569             }\r
570             $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));\r
571             $crcStored = unpack('v', substr($data, $headerLength, 2));\r
572             if ($crcReal != $crcStored[1]) {\r
573                 throw new HTTP_Request2_MessageException(\r
574                     'Header CRC check failed',\r
575                     HTTP_Request2_Exception::DECODE_ERROR\r
576                 );\r
577             }\r
578             $headerLength += 2;\r
579         }\r
580         // unpacked data CRC and size at the end of encoded data\r
581         $tmp = unpack('V2', substr($data, -8));\r
582         $dataCrc  = $tmp[1];\r
583         $dataSize = $tmp[2];\r
584 \r
585         // finally, call the gzinflate() function\r
586         // don't pass $dataSize to gzinflate, see bugs #13135, #14370\r
587         $unpacked = gzinflate(substr($data, $headerLength, -8));\r
588         if (false === $unpacked) {\r
589             throw new HTTP_Request2_MessageException(\r
590                 'gzinflate() call failed',\r
591                 HTTP_Request2_Exception::DECODE_ERROR\r
592             );\r
593         } elseif ($dataSize != strlen($unpacked)) {\r
594             throw new HTTP_Request2_MessageException(\r
595                 'Data size check failed',\r
596                 HTTP_Request2_Exception::DECODE_ERROR\r
597             );\r
598         } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {\r
599             throw new HTTP_Request2_Exception(\r
600                 'Data CRC check failed',\r
601                 HTTP_Request2_Exception::DECODE_ERROR\r
602             );\r
603         }\r
604         return $unpacked;\r
605     }\r
606 \r
607     /**\r
608      * Decodes the message-body encoded by deflate\r
609      *\r
610      * @param string $data deflate-encoded data\r
611      *\r
612      * @return   string  decoded data\r
613      * @throws   HTTP_Request2_LogicException\r
614      */\r
615     public static function decodeDeflate($data)\r
616     {\r
617         if (!function_exists('gzuncompress')) {\r
618             throw new HTTP_Request2_LogicException(\r
619                 'Unable to decode body: gzip extension not available',\r
620                 HTTP_Request2_Exception::MISCONFIGURATION\r
621             );\r
622         }\r
623         // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,\r
624         // while many applications send raw deflate stream from RFC 1951.\r
625         // We should check for presence of zlib header and use gzuncompress() or\r
626         // gzinflate() as needed. See bug #15305\r
627         $header = unpack('n', substr($data, 0, 2));\r
628         return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);\r
629     }\r
630 }\r
631 ?>