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