Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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