source: branches/version-2_13_0/data/module/HTTP/Request2.php @ 23126

Revision 23126, 37.3 KB checked in by m_uehara, 7 years ago (diff)

#2348 r23116 - r23125 をマージ

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