]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/HTTP/Request2.php
Merge branch 'master' of gitorious.org:social/mainline
[quix0rs-gnu-social.git] / extlib / HTTP / Request2.php
1 <?php\r
2 /**\r
3  * Class representing a HTTP request message\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  * A class representing an URL as per RFC 3986.\r
23  */\r
24 require_once 'Net/URL2.php';\r
25 \r
26 /**\r
27  * Exception class for HTTP_Request2 package\r
28  */\r
29 require_once 'HTTP/Request2/Exception.php';\r
30 \r
31 /**\r
32  * Class representing a HTTP request message\r
33  *\r
34  * @category HTTP\r
35  * @package  HTTP_Request2\r
36  * @author   Alexey Borzov <avb@php.net>\r
37  * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
38  * @version  Release: 2.2.1\r
39  * @link     http://pear.php.net/package/HTTP_Request2\r
40  * @link     http://tools.ietf.org/html/rfc2616#section-5\r
41  */\r
42 class HTTP_Request2 implements SplSubject\r
43 {\r
44     /**#@+\r
45      * Constants for HTTP request methods\r
46      *\r
47      * @link http://tools.ietf.org/html/rfc2616#section-5.1.1\r
48      */\r
49     const METHOD_OPTIONS = 'OPTIONS';\r
50     const METHOD_GET     = 'GET';\r
51     const METHOD_HEAD    = 'HEAD';\r
52     const METHOD_POST    = 'POST';\r
53     const METHOD_PUT     = 'PUT';\r
54     const METHOD_DELETE  = 'DELETE';\r
55     const METHOD_TRACE   = 'TRACE';\r
56     const METHOD_CONNECT = 'CONNECT';\r
57     /**#@-*/\r
58 \r
59     /**#@+\r
60      * Constants for HTTP authentication schemes\r
61      *\r
62      * @link http://tools.ietf.org/html/rfc2617\r
63      */\r
64     const AUTH_BASIC  = 'basic';\r
65     const AUTH_DIGEST = 'digest';\r
66     /**#@-*/\r
67 \r
68     /**\r
69      * Regular expression used to check for invalid symbols in RFC 2616 tokens\r
70      * @link http://pear.php.net/bugs/bug.php?id=15630\r
71      */\r
72     const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!';\r
73 \r
74     /**\r
75      * Regular expression used to check for invalid symbols in cookie strings\r
76      * @link http://pear.php.net/bugs/bug.php?id=15630\r
77      * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html\r
78      */\r
79     const REGEXP_INVALID_COOKIE = '/[\s,;]/';\r
80 \r
81     /**\r
82      * Fileinfo magic database resource\r
83      * @var  resource\r
84      * @see  detectMimeType()\r
85      */\r
86     private static $_fileinfoDb;\r
87 \r
88     /**\r
89      * Observers attached to the request (instances of SplObserver)\r
90      * @var  array\r
91      */\r
92     protected $observers = array();\r
93 \r
94     /**\r
95      * Request URL\r
96      * @var  Net_URL2\r
97      */\r
98     protected $url;\r
99 \r
100     /**\r
101      * Request method\r
102      * @var  string\r
103      */\r
104     protected $method = self::METHOD_GET;\r
105 \r
106     /**\r
107      * Authentication data\r
108      * @var  array\r
109      * @see  getAuth()\r
110      */\r
111     protected $auth;\r
112 \r
113     /**\r
114      * Request headers\r
115      * @var  array\r
116      */\r
117     protected $headers = array();\r
118 \r
119     /**\r
120      * Configuration parameters\r
121      * @var  array\r
122      * @see  setConfig()\r
123      */\r
124     protected $config = array(\r
125         'adapter'           => 'HTTP_Request2_Adapter_Socket',\r
126         'connect_timeout'   => 10,\r
127         'timeout'           => 0,\r
128         'use_brackets'      => true,\r
129         'protocol_version'  => '1.1',\r
130         'buffer_size'       => 16384,\r
131         'store_body'        => true,\r
132         'local_ip'          => null,\r
133 \r
134         'proxy_host'        => '',\r
135         'proxy_port'        => '',\r
136         'proxy_user'        => '',\r
137         'proxy_password'    => '',\r
138         'proxy_auth_scheme' => self::AUTH_BASIC,\r
139         'proxy_type'        => 'http',\r
140 \r
141         'ssl_verify_peer'   => true,\r
142         'ssl_verify_host'   => true,\r
143         'ssl_cafile'        => null,\r
144         'ssl_capath'        => null,\r
145         'ssl_local_cert'    => null,\r
146         'ssl_passphrase'    => null,\r
147 \r
148         'digest_compat_ie'  => false,\r
149 \r
150         'follow_redirects'  => false,\r
151         'max_redirects'     => 5,\r
152         'strict_redirects'  => false\r
153     );\r
154 \r
155     /**\r
156      * Last event in request / response handling, intended for observers\r
157      * @var  array\r
158      * @see  getLastEvent()\r
159      */\r
160     protected $lastEvent = array(\r
161         'name' => 'start',\r
162         'data' => null\r
163     );\r
164 \r
165     /**\r
166      * Request body\r
167      * @var  string|resource\r
168      * @see  setBody()\r
169      */\r
170     protected $body = '';\r
171 \r
172     /**\r
173      * Array of POST parameters\r
174      * @var  array\r
175      */\r
176     protected $postParams = array();\r
177 \r
178     /**\r
179      * Array of file uploads (for multipart/form-data POST requests)\r
180      * @var  array\r
181      */\r
182     protected $uploads = array();\r
183 \r
184     /**\r
185      * Adapter used to perform actual HTTP request\r
186      * @var  HTTP_Request2_Adapter\r
187      */\r
188     protected $adapter;\r
189 \r
190     /**\r
191      * Cookie jar to persist cookies between requests\r
192      * @var HTTP_Request2_CookieJar\r
193      */\r
194     protected $cookieJar = null;\r
195 \r
196     /**\r
197      * Constructor. Can set request URL, method and configuration array.\r
198      *\r
199      * Also sets a default value for User-Agent header.\r
200      *\r
201      * @param string|Net_Url2 $url    Request URL\r
202      * @param string          $method Request method\r
203      * @param array           $config Configuration for this Request instance\r
204      */\r
205     public function __construct(\r
206         $url = null, $method = self::METHOD_GET, array $config = array()\r
207     ) {\r
208         $this->setConfig($config);\r
209         if (!empty($url)) {\r
210             $this->setUrl($url);\r
211         }\r
212         if (!empty($method)) {\r
213             $this->setMethod($method);\r
214         }\r
215         $this->setHeader(\r
216             'user-agent', 'HTTP_Request2/2.2.1 ' .\r
217             '(http://pear.php.net/package/http_request2) PHP/' . phpversion()\r
218         );\r
219     }\r
220 \r
221     /**\r
222      * Sets the URL for this request\r
223      *\r
224      * If the URL has userinfo part (username & password) these will be removed\r
225      * and converted to auth data. If the URL does not have a path component,\r
226      * that will be set to '/'.\r
227      *\r
228      * @param string|Net_URL2 $url Request URL\r
229      *\r
230      * @return   HTTP_Request2\r
231      * @throws   HTTP_Request2_LogicException\r
232      */\r
233     public function setUrl($url)\r
234     {\r
235         if (is_string($url)) {\r
236             $url = new Net_URL2(\r
237                 $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets'])\r
238             );\r
239         }\r
240         if (!$url instanceof Net_URL2) {\r
241             throw new HTTP_Request2_LogicException(\r
242                 'Parameter is not a valid HTTP URL',\r
243                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
244             );\r
245         }\r
246         // URL contains username / password?\r
247         if ($url->getUserinfo()) {\r
248             $username = $url->getUser();\r
249             $password = $url->getPassword();\r
250             $this->setAuth(rawurldecode($username), $password? rawurldecode($password): '');\r
251             $url->setUserinfo('');\r
252         }\r
253         if ('' == $url->getPath()) {\r
254             $url->setPath('/');\r
255         }\r
256         $this->url = $url;\r
257 \r
258         return $this;\r
259     }\r
260 \r
261     /**\r
262      * Returns the request URL\r
263      *\r
264      * @return   Net_URL2\r
265      */\r
266     public function getUrl()\r
267     {\r
268         return $this->url;\r
269     }\r
270 \r
271     /**\r
272      * Sets the request method\r
273      *\r
274      * @param string $method one of the methods defined in RFC 2616\r
275      *\r
276      * @return   HTTP_Request2\r
277      * @throws   HTTP_Request2_LogicException if the method name is invalid\r
278      */\r
279     public function setMethod($method)\r
280     {\r
281         // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1\r
282         if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) {\r
283             throw new HTTP_Request2_LogicException(\r
284                 "Invalid request method '{$method}'",\r
285                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
286             );\r
287         }\r
288         $this->method = $method;\r
289 \r
290         return $this;\r
291     }\r
292 \r
293     /**\r
294      * Returns the request method\r
295      *\r
296      * @return   string\r
297      */\r
298     public function getMethod()\r
299     {\r
300         return $this->method;\r
301     }\r
302 \r
303     /**\r
304      * Sets the configuration parameter(s)\r
305      *\r
306      * The following parameters are available:\r
307      * <ul>\r
308      *   <li> 'adapter'           - adapter to use (string)</li>\r
309      *   <li> 'connect_timeout'   - Connection timeout in seconds (integer)</li>\r
310      *   <li> 'timeout'           - Total number of seconds a request can take.\r
311      *                              Use 0 for no limit, should be greater than\r
312      *                              'connect_timeout' if set (integer)</li>\r
313      *   <li> 'use_brackets'      - Whether to append [] to array variable names (bool)</li>\r
314      *   <li> 'protocol_version'  - HTTP Version to use, '1.0' or '1.1' (string)</li>\r
315      *   <li> 'buffer_size'       - Buffer size to use for reading and writing (int)</li>\r
316      *   <li> 'store_body'        - Whether to store response body in response object.\r
317      *                              Set to false if receiving a huge response and\r
318      *                              using an Observer to save it (boolean)</li>\r
319      *   <li> 'local_ip'          - Specifies the IP address that will be used for accessing\r
320      *                              the network (string)</li>\r
321      *   <li> 'proxy_type'        - Proxy type, 'http' or 'socks5' (string)</li>\r
322      *   <li> 'proxy_host'        - Proxy server host (string)</li>\r
323      *   <li> 'proxy_port'        - Proxy server port (integer)</li>\r
324      *   <li> 'proxy_user'        - Proxy auth username (string)</li>\r
325      *   <li> 'proxy_password'    - Proxy auth password (string)</li>\r
326      *   <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li>\r
327      *   <li> 'proxy'             - Shorthand for proxy_* parameters, proxy given as URL,\r
328      *                              e.g. 'socks5://localhost:1080/' (string)</li>\r
329      *   <li> 'ssl_verify_peer'   - Whether to verify peer's SSL certificate (bool)</li>\r
330      *   <li> 'ssl_verify_host'   - Whether to check that Common Name in SSL\r
331      *                              certificate matches host name (bool)</li>\r
332      *   <li> 'ssl_cafile'        - Cerificate Authority file to verify the peer\r
333      *                              with (use with 'ssl_verify_peer') (string)</li>\r
334      *   <li> 'ssl_capath'        - Directory holding multiple Certificate\r
335      *                              Authority files (string)</li>\r
336      *   <li> 'ssl_local_cert'    - Name of a file containing local cerificate (string)</li>\r
337      *   <li> 'ssl_passphrase'    - Passphrase with which local certificate\r
338      *                              was encoded (string)</li>\r
339      *   <li> 'digest_compat_ie'  - Whether to imitate behaviour of MSIE 5 and 6\r
340      *                              in using URL without query string in digest\r
341      *                              authentication (boolean)</li>\r
342      *   <li> 'follow_redirects'  - Whether to automatically follow HTTP Redirects (boolean)</li>\r
343      *   <li> 'max_redirects'     - Maximum number of redirects to follow (integer)</li>\r
344      *   <li> 'strict_redirects'  - Whether to keep request method on redirects via status 301 and\r
345      *                              302 (true, needed for compatibility with RFC 2616)\r
346      *                              or switch to GET (false, needed for compatibility with most\r
347      *                              browsers) (boolean)</li>\r
348      * </ul>\r
349      *\r
350      * @param string|array $nameOrConfig configuration parameter name or array\r
351      *                                   ('parameter name' => 'parameter value')\r
352      * @param mixed        $value        parameter value if $nameOrConfig is not an array\r
353      *\r
354      * @return   HTTP_Request2\r
355      * @throws   HTTP_Request2_LogicException If the parameter is unknown\r
356      */\r
357     public function setConfig($nameOrConfig, $value = null)\r
358     {\r
359         if (is_array($nameOrConfig)) {\r
360             foreach ($nameOrConfig as $name => $value) {\r
361                 $this->setConfig($name, $value);\r
362             }\r
363 \r
364         } elseif ('proxy' == $nameOrConfig) {\r
365             $url = new Net_URL2($value);\r
366             $this->setConfig(array(\r
367                 'proxy_type'     => $url->getScheme(),\r
368                 'proxy_host'     => $url->getHost(),\r
369                 'proxy_port'     => $url->getPort(),\r
370                 'proxy_user'     => rawurldecode($url->getUser()),\r
371                 'proxy_password' => rawurldecode($url->getPassword())\r
372             ));\r
373 \r
374         } else {\r
375             if (!array_key_exists($nameOrConfig, $this->config)) {\r
376                 throw new HTTP_Request2_LogicException(\r
377                     "Unknown configuration parameter '{$nameOrConfig}'",\r
378                     HTTP_Request2_Exception::INVALID_ARGUMENT\r
379                 );\r
380             }\r
381             $this->config[$nameOrConfig] = $value;\r
382         }\r
383 \r
384         return $this;\r
385     }\r
386 \r
387     /**\r
388      * Returns the value(s) of the configuration parameter(s)\r
389      *\r
390      * @param string $name parameter name\r
391      *\r
392      * @return   mixed   value of $name parameter, array of all configuration\r
393      *                   parameters if $name is not given\r
394      * @throws   HTTP_Request2_LogicException If the parameter is unknown\r
395      */\r
396     public function getConfig($name = null)\r
397     {\r
398         if (null === $name) {\r
399             return $this->config;\r
400         } elseif (!array_key_exists($name, $this->config)) {\r
401             throw new HTTP_Request2_LogicException(\r
402                 "Unknown configuration parameter '{$name}'",\r
403                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
404             );\r
405         }\r
406         return $this->config[$name];\r
407     }\r
408 \r
409     /**\r
410      * Sets the autentification data\r
411      *\r
412      * @param string $user     user name\r
413      * @param string $password password\r
414      * @param string $scheme   authentication scheme\r
415      *\r
416      * @return   HTTP_Request2\r
417      */\r
418     public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)\r
419     {\r
420         if (empty($user)) {\r
421             $this->auth = null;\r
422         } else {\r
423             $this->auth = array(\r
424                 'user'     => (string)$user,\r
425                 'password' => (string)$password,\r
426                 'scheme'   => $scheme\r
427             );\r
428         }\r
429 \r
430         return $this;\r
431     }\r
432 \r
433     /**\r
434      * Returns the authentication data\r
435      *\r
436      * The array has the keys 'user', 'password' and 'scheme', where 'scheme'\r
437      * is one of the HTTP_Request2::AUTH_* constants.\r
438      *\r
439      * @return   array\r
440      */\r
441     public function getAuth()\r
442     {\r
443         return $this->auth;\r
444     }\r
445 \r
446     /**\r
447      * Sets request header(s)\r
448      *\r
449      * The first parameter may be either a full header string 'header: value' or\r
450      * header name. In the former case $value parameter is ignored, in the latter\r
451      * the header's value will either be set to $value or the header will be\r
452      * removed if $value is null. The first parameter can also be an array of\r
453      * headers, in that case method will be called recursively.\r
454      *\r
455      * Note that headers are treated case insensitively as per RFC 2616.\r
456      *\r
457      * <code>\r
458      * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'\r
459      * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'\r
460      * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux'\r
461      * $req->setHeader('FOO'); // removes 'Foo' header from request\r
462      * </code>\r
463      *\r
464      * @param string|array      $name    header name, header string ('Header: value')\r
465      *                                   or an array of headers\r
466      * @param string|array|null $value   header value if $name is not an array,\r
467      *                                   header will be removed if value is null\r
468      * @param bool              $replace whether to replace previous header with the\r
469      *                                   same name or append to its value\r
470      *\r
471      * @return   HTTP_Request2\r
472      * @throws   HTTP_Request2_LogicException\r
473      */\r
474     public function setHeader($name, $value = null, $replace = true)\r
475     {\r
476         if (is_array($name)) {\r
477             foreach ($name as $k => $v) {\r
478                 if (is_string($k)) {\r
479                     $this->setHeader($k, $v, $replace);\r
480                 } else {\r
481                     $this->setHeader($v, null, $replace);\r
482                 }\r
483             }\r
484         } else {\r
485             if (null === $value && strpos($name, ':')) {\r
486                 list($name, $value) = array_map('trim', explode(':', $name, 2));\r
487             }\r
488             // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2\r
489             if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) {\r
490                 throw new HTTP_Request2_LogicException(\r
491                     "Invalid header name '{$name}'",\r
492                     HTTP_Request2_Exception::INVALID_ARGUMENT\r
493                 );\r
494             }\r
495             // Header names are case insensitive anyway\r
496             $name = strtolower($name);\r
497             if (null === $value) {\r
498                 unset($this->headers[$name]);\r
499 \r
500             } else {\r
501                 if (is_array($value)) {\r
502                     $value = implode(', ', array_map('trim', $value));\r
503                 } elseif (is_string($value)) {\r
504                     $value = trim($value);\r
505                 }\r
506                 if (!isset($this->headers[$name]) || $replace) {\r
507                     $this->headers[$name] = $value;\r
508                 } else {\r
509                     $this->headers[$name] .= ', ' . $value;\r
510                 }\r
511             }\r
512         }\r
513 \r
514         return $this;\r
515     }\r
516 \r
517     /**\r
518      * Returns the request headers\r
519      *\r
520      * The array is of the form ('header name' => 'header value'), header names\r
521      * are lowercased\r
522      *\r
523      * @return   array\r
524      */\r
525     public function getHeaders()\r
526     {\r
527         return $this->headers;\r
528     }\r
529 \r
530     /**\r
531      * Adds a cookie to the request\r
532      *\r
533      * If the request does not have a CookieJar object set, this method simply\r
534      * appends a cookie to "Cookie:" header.\r
535      *\r
536      * If a CookieJar object is available, the cookie is stored in that object.\r
537      * Data from request URL will be used for setting its 'domain' and 'path'\r
538      * parameters, 'expires' and 'secure' will be set to null and false,\r
539      * respectively. If you need further control, use CookieJar's methods.\r
540      *\r
541      * @param string $name  cookie name\r
542      * @param string $value cookie value\r
543      *\r
544      * @return   HTTP_Request2\r
545      * @throws   HTTP_Request2_LogicException\r
546      * @see      setCookieJar()\r
547      */\r
548     public function addCookie($name, $value)\r
549     {\r
550         if (!empty($this->cookieJar)) {\r
551             $this->cookieJar->store(\r
552                 array('name' => $name, 'value' => $value), $this->url\r
553             );\r
554 \r
555         } else {\r
556             $cookie = $name . '=' . $value;\r
557             if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {\r
558                 throw new HTTP_Request2_LogicException(\r
559                     "Invalid cookie: '{$cookie}'",\r
560                     HTTP_Request2_Exception::INVALID_ARGUMENT\r
561                 );\r
562             }\r
563             $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';\r
564             $this->setHeader('cookie', $cookies . $cookie);\r
565         }\r
566 \r
567         return $this;\r
568     }\r
569 \r
570     /**\r
571      * Sets the request body\r
572      *\r
573      * If you provide file pointer rather than file name, it should support\r
574      * fstat() and rewind() operations.\r
575      *\r
576      * @param string|resource|HTTP_Request2_MultipartBody $body       Either a\r
577      *               string with the body or filename containing body or\r
578      *               pointer to an open file or object with multipart body data\r
579      * @param bool                                        $isFilename Whether\r
580      *               first parameter is a filename\r
581      *\r
582      * @return   HTTP_Request2\r
583      * @throws   HTTP_Request2_LogicException\r
584      */\r
585     public function setBody($body, $isFilename = false)\r
586     {\r
587         if (!$isFilename && !is_resource($body)) {\r
588             if (!$body instanceof HTTP_Request2_MultipartBody) {\r
589                 $this->body = (string)$body;\r
590             } else {\r
591                 $this->body = $body;\r
592             }\r
593         } else {\r
594             $fileData = $this->fopenWrapper($body, empty($this->headers['content-type']));\r
595             $this->body = $fileData['fp'];\r
596             if (empty($this->headers['content-type'])) {\r
597                 $this->setHeader('content-type', $fileData['type']);\r
598             }\r
599         }\r
600         $this->postParams = $this->uploads = array();\r
601 \r
602         return $this;\r
603     }\r
604 \r
605     /**\r
606      * Returns the request body\r
607      *\r
608      * @return   string|resource|HTTP_Request2_MultipartBody\r
609      */\r
610     public function getBody()\r
611     {\r
612         if (self::METHOD_POST == $this->method\r
613             && (!empty($this->postParams) || !empty($this->uploads))\r
614         ) {\r
615             if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) {\r
616                 $body = http_build_query($this->postParams, '', '&');\r
617                 if (!$this->getConfig('use_brackets')) {\r
618                     $body = preg_replace('/%5B\d+%5D=/', '=', $body);\r
619                 }\r
620                 // support RFC 3986 by not encoding '~' symbol (request #15368)\r
621                 return str_replace('%7E', '~', $body);\r
622 \r
623             } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) {\r
624                 require_once 'HTTP/Request2/MultipartBody.php';\r
625                 return new HTTP_Request2_MultipartBody(\r
626                     $this->postParams, $this->uploads, $this->getConfig('use_brackets')\r
627                 );\r
628             }\r
629         }\r
630         return $this->body;\r
631     }\r
632 \r
633     /**\r
634      * Adds a file to form-based file upload\r
635      *\r
636      * Used to emulate file upload via a HTML form. The method also sets\r
637      * Content-Type of HTTP request to 'multipart/form-data'.\r
638      *\r
639      * If you just want to send the contents of a file as the body of HTTP\r
640      * request you should use setBody() method.\r
641      *\r
642      * If you provide file pointers rather than file names, they should support\r
643      * fstat() and rewind() operations.\r
644      *\r
645      * @param string                $fieldName    name of file-upload field\r
646      * @param string|resource|array $filename     full name of local file,\r
647      *               pointer to open file or an array of files\r
648      * @param string                $sendFilename filename to send in the request\r
649      * @param string                $contentType  content-type of file being uploaded\r
650      *\r
651      * @return   HTTP_Request2\r
652      * @throws   HTTP_Request2_LogicException\r
653      */\r
654     public function addUpload(\r
655         $fieldName, $filename, $sendFilename = null, $contentType = null\r
656     ) {\r
657         if (!is_array($filename)) {\r
658             $fileData = $this->fopenWrapper($filename, empty($contentType));\r
659             $this->uploads[$fieldName] = array(\r
660                 'fp'        => $fileData['fp'],\r
661                 'filename'  => !empty($sendFilename)? $sendFilename\r
662                                 :(is_string($filename)? basename($filename): 'anonymous.blob') ,\r
663                 'size'      => $fileData['size'],\r
664                 'type'      => empty($contentType)? $fileData['type']: $contentType\r
665             );\r
666         } else {\r
667             $fps = $names = $sizes = $types = array();\r
668             foreach ($filename as $f) {\r
669                 if (!is_array($f)) {\r
670                     $f = array($f);\r
671                 }\r
672                 $fileData = $this->fopenWrapper($f[0], empty($f[2]));\r
673                 $fps[]   = $fileData['fp'];\r
674                 $names[] = !empty($f[1])? $f[1]\r
675                             :(is_string($f[0])? basename($f[0]): 'anonymous.blob');\r
676                 $sizes[] = $fileData['size'];\r
677                 $types[] = empty($f[2])? $fileData['type']: $f[2];\r
678             }\r
679             $this->uploads[$fieldName] = array(\r
680                 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types\r
681             );\r
682         }\r
683         if (empty($this->headers['content-type'])\r
684             || 'application/x-www-form-urlencoded' == $this->headers['content-type']\r
685         ) {\r
686             $this->setHeader('content-type', 'multipart/form-data');\r
687         }\r
688 \r
689         return $this;\r
690     }\r
691 \r
692     /**\r
693      * Adds POST parameter(s) to the request.\r
694      *\r
695      * @param string|array $name  parameter name or array ('name' => 'value')\r
696      * @param mixed        $value parameter value (can be an array)\r
697      *\r
698      * @return   HTTP_Request2\r
699      */\r
700     public function addPostParameter($name, $value = null)\r
701     {\r
702         if (!is_array($name)) {\r
703             $this->postParams[$name] = $value;\r
704         } else {\r
705             foreach ($name as $k => $v) {\r
706                 $this->addPostParameter($k, $v);\r
707             }\r
708         }\r
709         if (empty($this->headers['content-type'])) {\r
710             $this->setHeader('content-type', 'application/x-www-form-urlencoded');\r
711         }\r
712 \r
713         return $this;\r
714     }\r
715 \r
716     /**\r
717      * Attaches a new observer\r
718      *\r
719      * @param SplObserver $observer any object implementing SplObserver\r
720      */\r
721     public function attach(SplObserver $observer)\r
722     {\r
723         foreach ($this->observers as $attached) {\r
724             if ($attached === $observer) {\r
725                 return;\r
726             }\r
727         }\r
728         $this->observers[] = $observer;\r
729     }\r
730 \r
731     /**\r
732      * Detaches an existing observer\r
733      *\r
734      * @param SplObserver $observer any object implementing SplObserver\r
735      */\r
736     public function detach(SplObserver $observer)\r
737     {\r
738         foreach ($this->observers as $key => $attached) {\r
739             if ($attached === $observer) {\r
740                 unset($this->observers[$key]);\r
741                 return;\r
742             }\r
743         }\r
744     }\r
745 \r
746     /**\r
747      * Notifies all observers\r
748      */\r
749     public function notify()\r
750     {\r
751         foreach ($this->observers as $observer) {\r
752             $observer->update($this);\r
753         }\r
754     }\r
755 \r
756     /**\r
757      * Sets the last event\r
758      *\r
759      * Adapters should use this method to set the current state of the request\r
760      * and notify the observers.\r
761      *\r
762      * @param string $name event name\r
763      * @param mixed  $data event data\r
764      */\r
765     public function setLastEvent($name, $data = null)\r
766     {\r
767         $this->lastEvent = array(\r
768             'name' => $name,\r
769             'data' => $data\r
770         );\r
771         $this->notify();\r
772     }\r
773 \r
774     /**\r
775      * Returns the last event\r
776      *\r
777      * Observers should use this method to access the last change in request.\r
778      * The following event names are possible:\r
779      * <ul>\r
780      *   <li>'connect'                 - after connection to remote server,\r
781      *                                   data is the destination (string)</li>\r
782      *   <li>'disconnect'              - after disconnection from server</li>\r
783      *   <li>'sentHeaders'             - after sending the request headers,\r
784      *                                   data is the headers sent (string)</li>\r
785      *   <li>'sentBodyPart'            - after sending a part of the request body,\r
786      *                                   data is the length of that part (int)</li>\r
787      *   <li>'sentBody'                - after sending the whole request body,\r
788      *                                   data is request body length (int)</li>\r
789      *   <li>'receivedHeaders'         - after receiving the response headers,\r
790      *                                   data is HTTP_Request2_Response object</li>\r
791      *   <li>'receivedBodyPart'        - after receiving a part of the response\r
792      *                                   body, data is that part (string)</li>\r
793      *   <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still\r
794      *                                   encoded by Content-Encoding</li>\r
795      *   <li>'receivedBody'            - after receiving the complete response\r
796      *                                   body, data is HTTP_Request2_Response object</li>\r
797      * </ul>\r
798      * Different adapters may not send all the event types. Mock adapter does\r
799      * not send any events to the observers.\r
800      *\r
801      * @return   array   The array has two keys: 'name' and 'data'\r
802      */\r
803     public function getLastEvent()\r
804     {\r
805         return $this->lastEvent;\r
806     }\r
807 \r
808     /**\r
809      * Sets the adapter used to actually perform the request\r
810      *\r
811      * You can pass either an instance of a class implementing HTTP_Request2_Adapter\r
812      * or a class name. The method will only try to include a file if the class\r
813      * name starts with HTTP_Request2_Adapter_, it will also try to prepend this\r
814      * prefix to the class name if it doesn't contain any underscores, so that\r
815      * <code>\r
816      * $request->setAdapter('curl');\r
817      * </code>\r
818      * will work.\r
819      *\r
820      * @param string|HTTP_Request2_Adapter $adapter Adapter to use\r
821      *\r
822      * @return   HTTP_Request2\r
823      * @throws   HTTP_Request2_LogicException\r
824      */\r
825     public function setAdapter($adapter)\r
826     {\r
827         if (is_string($adapter)) {\r
828             if (!class_exists($adapter, false)) {\r
829                 if (false === strpos($adapter, '_')) {\r
830                     $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter);\r
831                 }\r
832                 if (!class_exists($adapter, false)\r
833                     && preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)\r
834                 ) {\r
835                     include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php';\r
836                 }\r
837                 if (!class_exists($adapter, false)) {\r
838                     throw new HTTP_Request2_LogicException(\r
839                         "Class {$adapter} not found",\r
840                         HTTP_Request2_Exception::MISSING_VALUE\r
841                     );\r
842                 }\r
843             }\r
844             $adapter = new $adapter;\r
845         }\r
846         if (!$adapter instanceof HTTP_Request2_Adapter) {\r
847             throw new HTTP_Request2_LogicException(\r
848                 'Parameter is not a HTTP request adapter',\r
849                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
850             );\r
851         }\r
852         $this->adapter = $adapter;\r
853 \r
854         return $this;\r
855     }\r
856 \r
857     /**\r
858      * Sets the cookie jar\r
859      *\r
860      * A cookie jar is used to maintain cookies across HTTP requests and\r
861      * responses. Cookies from jar will be automatically added to the request\r
862      * headers based on request URL.\r
863      *\r
864      * @param HTTP_Request2_CookieJar|bool $jar Existing CookieJar object, true to\r
865      *                                          create a new one, false to remove\r
866      *\r
867      * @return HTTP_Request2\r
868      * @throws HTTP_Request2_LogicException\r
869      */\r
870     public function setCookieJar($jar = true)\r
871     {\r
872         if (!class_exists('HTTP_Request2_CookieJar', false)) {\r
873             require_once 'HTTP/Request2/CookieJar.php';\r
874         }\r
875 \r
876         if ($jar instanceof HTTP_Request2_CookieJar) {\r
877             $this->cookieJar = $jar;\r
878         } elseif (true === $jar) {\r
879             $this->cookieJar = new HTTP_Request2_CookieJar();\r
880         } elseif (!$jar) {\r
881             $this->cookieJar = null;\r
882         } else {\r
883             throw new HTTP_Request2_LogicException(\r
884                 'Invalid parameter passed to setCookieJar()',\r
885                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
886             );\r
887         }\r
888 \r
889         return $this;\r
890     }\r
891 \r
892     /**\r
893      * Returns current CookieJar object or null if none\r
894      *\r
895      * @return HTTP_Request2_CookieJar|null\r
896      */\r
897     public function getCookieJar()\r
898     {\r
899         return $this->cookieJar;\r
900     }\r
901 \r
902     /**\r
903      * Sends the request and returns the response\r
904      *\r
905      * @throws   HTTP_Request2_Exception\r
906      * @return   HTTP_Request2_Response\r
907      */\r
908     public function send()\r
909     {\r
910         // Sanity check for URL\r
911         if (!$this->url instanceof Net_URL2\r
912             || !$this->url->isAbsolute()\r
913             || !in_array(strtolower($this->url->getScheme()), array('https', 'http'))\r
914         ) {\r
915             throw new HTTP_Request2_LogicException(\r
916                 'HTTP_Request2 needs an absolute HTTP(S) request URL, '\r
917                 . ($this->url instanceof Net_URL2\r
918                    ? "'" . $this->url->__toString() . "'" : 'none')\r
919                 . ' given',\r
920                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
921             );\r
922         }\r
923         if (empty($this->adapter)) {\r
924             $this->setAdapter($this->getConfig('adapter'));\r
925         }\r
926         // magic_quotes_runtime may break file uploads and chunked response\r
927         // processing; see bug #4543. Don't use ini_get() here; see bug #16440.\r
928         if ($magicQuotes = get_magic_quotes_runtime()) {\r
929             set_magic_quotes_runtime(false);\r
930         }\r
931         // force using single byte encoding if mbstring extension overloads\r
932         // strlen() and substr(); see bug #1781, bug #10605\r
933         if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {\r
934             $oldEncoding = mb_internal_encoding();\r
935             mb_internal_encoding('8bit');\r
936         }\r
937 \r
938         try {\r
939             $response = $this->adapter->sendRequest($this);\r
940         } catch (Exception $e) {\r
941         }\r
942         // cleanup in either case (poor man's "finally" clause)\r
943         if ($magicQuotes) {\r
944             set_magic_quotes_runtime(true);\r
945         }\r
946         if (!empty($oldEncoding)) {\r
947             mb_internal_encoding($oldEncoding);\r
948         }\r
949         // rethrow the exception\r
950         if (!empty($e)) {\r
951             throw $e;\r
952         }\r
953         return $response;\r
954     }\r
955 \r
956     /**\r
957      * Wrapper around fopen()/fstat() used by setBody() and addUpload()\r
958      *\r
959      * @param string|resource $file       file name or pointer to open file\r
960      * @param bool            $detectType whether to try autodetecting MIME\r
961      *                        type of file, will only work if $file is a\r
962      *                        filename, not pointer\r
963      *\r
964      * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type)\r
965      * @throws HTTP_Request2_LogicException\r
966      */\r
967     protected function fopenWrapper($file, $detectType = false)\r
968     {\r
969         if (!is_string($file) && !is_resource($file)) {\r
970             throw new HTTP_Request2_LogicException(\r
971                 "Filename or file pointer resource expected",\r
972                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
973             );\r
974         }\r
975         $fileData = array(\r
976             'fp'   => is_string($file)? null: $file,\r
977             'type' => 'application/octet-stream',\r
978             'size' => 0\r
979         );\r
980         if (is_string($file)) {\r
981             if (!($fileData['fp'] = @fopen($file, 'rb'))) {\r
982                 $error = error_get_last();\r
983                 throw new HTTP_Request2_LogicException(\r
984                     $error['message'], HTTP_Request2_Exception::READ_ERROR\r
985                 );\r
986             }\r
987             if ($detectType) {\r
988                 $fileData['type'] = self::detectMimeType($file);\r
989             }\r
990         }\r
991         if (!($stat = fstat($fileData['fp']))) {\r
992             throw new HTTP_Request2_LogicException(\r
993                 "fstat() call failed", HTTP_Request2_Exception::READ_ERROR\r
994             );\r
995         }\r
996         $fileData['size'] = $stat['size'];\r
997 \r
998         return $fileData;\r
999     }\r
1000 \r
1001     /**\r
1002      * Tries to detect MIME type of a file\r
1003      *\r
1004      * The method will try to use fileinfo extension if it is available,\r
1005      * deprecated mime_content_type() function in the other case. If neither\r
1006      * works, default 'application/octet-stream' MIME type is returned\r
1007      *\r
1008      * @param string $filename file name\r
1009      *\r
1010      * @return   string  file MIME type\r
1011      */\r
1012     protected static function detectMimeType($filename)\r
1013     {\r
1014         // finfo extension from PECL available\r
1015         if (function_exists('finfo_open')) {\r
1016             if (!isset(self::$_fileinfoDb)) {\r
1017                 self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);\r
1018             }\r
1019             if (self::$_fileinfoDb) {\r
1020                 $info = finfo_file(self::$_fileinfoDb, $filename);\r
1021             }\r
1022         }\r
1023         // (deprecated) mime_content_type function available\r
1024         if (empty($info) && function_exists('mime_content_type')) {\r
1025             return mime_content_type($filename);\r
1026         }\r
1027         return empty($info)? 'application/octet-stream': $info;\r
1028     }\r
1029 }\r
1030 ?>\r