source: temp/trunk/data/module/Request.php @ 8273

Revision 8273, 45.1 KB checked in by kakinaka, 20 years ago (diff)

blank

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Copyright (c) 2002-2003, Richard Heyes                                |
4// | All rights reserved.                                                  |
5// |                                                                       |
6// | Redistribution and use in source and binary forms, with or without    |
7// | modification, are permitted provided that the following conditions    |
8// | are met:                                                              |
9// |                                                                       |
10// | o Redistributions of source code must retain the above copyright      |
11// |   notice, this list of conditions and the following disclaimer.       |
12// | o Redistributions in binary form must reproduce the above copyright   |
13// |   notice, this list of conditions and the following disclaimer in the |
14// |   documentation and/or other materials provided with the distribution.|
15// | o The names of the authors may not be used to endorse or promote      |
16// |   products derived from this software without specific prior written  |
17// |   permission.                                                         |
18// |                                                                       |
19// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
20// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
21// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
22// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
23// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
24// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
25// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
26// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
27// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
28// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
29// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
30// |                                                                       |
31// +-----------------------------------------------------------------------+
32// | Author: Richard Heyes <[email protected]>                           |
33// +-----------------------------------------------------------------------+
34//
35// $Id$
36//
37// HTTP_Request Class
38//
39// Simple example, (Fetches yahoo.com and displays it):
40//
41// $a = &new HTTP_Request('http://www.yahoo.com/');
42// $a->sendRequest();
43// echo $a->getResponseBody();
44//
45
46if(!defined('REQUEST_PHP_DIR')) {
47    $REQUEST_PHP_DIR = realpath(dirname( __FILE__));
48    define("REQUEST_PHP_DIR", $REQUEST_PHP_DIR);   
49}
50
51require_once REQUEST_PHP_DIR . '/PEAR.php';
52require_once REQUEST_PHP_DIR . '/Net/Socket.php';
53require_once REQUEST_PHP_DIR . '/Net/URL.php';
54
55define('HTTP_REQUEST_METHOD_GET',     'GET',     true);
56define('HTTP_REQUEST_METHOD_HEAD',    'HEAD',    true);
57define('HTTP_REQUEST_METHOD_POST',    'POST',    true);
58define('HTTP_REQUEST_METHOD_PUT',     'PUT',     true);
59define('HTTP_REQUEST_METHOD_DELETE',  'DELETE',  true);
60define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
61define('HTTP_REQUEST_METHOD_TRACE',   'TRACE',   true);
62
63define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
64define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);
65
66class HTTP_Request {
67
68    /**
69    * Instance of Net_URL
70    * @var object Net_URL
71    */
72    var $_url;
73
74    /**
75    * Type of request
76    * @var string
77    */
78    var $_method;
79
80    /**
81    * HTTP Version
82    * @var string
83    */
84    var $_http;
85
86    /**
87    * Request headers
88    * @var array
89    */
90    var $_requestHeaders;
91
92    /**
93    * Basic Auth Username
94    * @var string
95    */
96    var $_user;
97   
98    /**
99    * Basic Auth Password
100    * @var string
101    */
102    var $_pass;
103
104    /**
105    * Socket object
106    * @var object Net_Socket
107    */
108    var $_sock;
109   
110    /**
111    * Proxy server
112    * @var string
113    */
114    var $_proxy_host;
115   
116    /**
117    * Proxy port
118    * @var integer
119    */
120    var $_proxy_port;
121   
122    /**
123    * Proxy username
124    * @var string
125    */
126    var $_proxy_user;
127   
128    /**
129    * Proxy password
130    * @var string
131    */
132    var $_proxy_pass;
133
134    /**
135    * Post data
136    * @var array
137    */
138    var $_postData;
139
140   /**
141    * Request body 
142    * @var string
143    */
144    var $_body;
145
146   /**
147    * A list of methods that MUST NOT have a request body, per RFC 2616
148    * @var array
149    */
150    var $_bodyDisallowed = array('TRACE');
151
152   /**
153    * Files to post
154    * @var array
155    */
156    var $_postFiles = array();
157
158    /**
159    * Connection timeout.
160    * @var float
161    */
162    var $_timeout;
163   
164    /**
165    * HTTP_Response object
166    * @var object HTTP_Response
167    */
168    var $_response;
169   
170    /**
171    * Whether to allow redirects
172    * @var boolean
173    */
174    var $_allowRedirects;
175   
176    /**
177    * Maximum redirects allowed
178    * @var integer
179    */
180    var $_maxRedirects;
181   
182    /**
183    * Current number of redirects
184    * @var integer
185    */
186    var $_redirects;
187
188   /**
189    * Whether to append brackets [] to array variables
190    * @var bool
191    */
192    var $_useBrackets = true;
193
194   /**
195    * Attached listeners
196    * @var array
197    */
198    var $_listeners = array();
199
200   /**
201    * Whether to save response body in response object property 
202    * @var bool
203    */
204    var $_saveBody = true;
205
206   /**
207    * Timeout for reading from socket (array(seconds, microseconds))
208    * @var array
209    */
210    var $_readTimeout = null;
211
212   /**
213    * Options to pass to Net_Socket::connect. See stream_context_create
214    * @var array
215    */
216    var $_socketOptions = null;
217
218    /**
219    * Constructor
220    *
221    * Sets up the object
222    * @param    string  The url to fetch/access
223    * @param    array   Associative array of parameters which can have the following keys:
224    * <ul>
225    *   <li>method         - Method to use, GET, POST etc (string)</li>
226    *   <li>http           - HTTP Version to use, 1.0 or 1.1 (string)</li>
227    *   <li>user           - Basic Auth username (string)</li>
228    *   <li>pass           - Basic Auth password (string)</li>
229    *   <li>proxy_host     - Proxy server host (string)</li>
230    *   <li>proxy_port     - Proxy server port (integer)</li>
231    *   <li>proxy_user     - Proxy auth username (string)</li>
232    *   <li>proxy_pass     - Proxy auth password (string)</li>
233    *   <li>timeout        - Connection timeout in seconds (float)</li>
234    *   <li>allowRedirects - Whether to follow redirects or not (bool)</li>
235    *   <li>maxRedirects   - Max number of redirects to follow (integer)</li>
236    *   <li>useBrackets    - Whether to append [] to array variable names (bool)</li>
237    *   <li>saveBody       - Whether to save response body in response object property (bool)</li>
238    *   <li>readTimeout    - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>
239    *   <li>socketOptions  - Options to pass to Net_Socket object (array)</li>
240    * </ul>
241    * @access public
242    */
243    function HTTP_Request($url = '', $params = array())
244    {
245        $this->_method         =  HTTP_REQUEST_METHOD_GET;
246        $this->_http           =  HTTP_REQUEST_HTTP_VER_1_1;
247        $this->_requestHeaders = array();
248        $this->_postData       = array();
249        $this->_body           = null;
250
251        $this->_user = null;
252        $this->_pass = null;
253
254        $this->_proxy_host = null;
255        $this->_proxy_port = null;
256        $this->_proxy_user = null;
257        $this->_proxy_pass = null;
258
259        $this->_allowRedirects = false;
260        $this->_maxRedirects   = 3;
261        $this->_redirects      = 0;
262
263        $this->_timeout  = null;
264        $this->_response = null;
265
266        foreach ($params as $key => $value) {
267            $this->{'_' . $key} = $value;
268        }
269
270        if (!empty($url)) {
271            $this->setURL($url);
272        }
273
274        // Default useragent
275        $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');
276
277        // We don't do keep-alives by default
278        $this->addHeader('Connection', 'close');
279
280        // Basic authentication
281        if (!empty($this->_user)) {
282            $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));
283        }
284
285        // Proxy authentication (see bug #5913)
286        if (!empty($this->_proxy_user)) {
287            $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass));
288        }
289
290        // Use gzip encoding if possible
291        // Avoid gzip encoding if using multibyte functions (see #1781)
292        if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib') &&
293            0 == (2 & ini_get('mbstring.func_overload'))) {
294
295            $this->addHeader('Accept-Encoding', 'gzip');
296        }
297    }
298   
299    /**
300    * Generates a Host header for HTTP/1.1 requests
301    *
302    * @access private
303    * @return string
304    */
305    function _generateHostHeader()
306    {
307        if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {
308            $host = $this->_url->host . ':' . $this->_url->port;
309
310        } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {
311            $host = $this->_url->host . ':' . $this->_url->port;
312
313        } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {
314            $host = $this->_url->host . ':' . $this->_url->port;
315       
316        } else {
317            $host = $this->_url->host;
318        }
319
320        return $host;
321    }
322   
323    /**
324    * Resets the object to its initial state (DEPRECATED).
325    * Takes the same parameters as the constructor.
326    *
327    * @param  string $url    The url to be requested
328    * @param  array  $params Associative array of parameters
329    *                        (see constructor for details)
330    * @access public
331    * @deprecated deprecated since 1.2, call the constructor if this is necessary
332    */
333    function reset($url, $params = array())
334    {
335        $this->HTTP_Request($url, $params);
336    }
337
338    /**
339    * Sets the URL to be requested
340    *
341    * @param  string The url to be requested
342    * @access public
343    */
344    function setURL($url)
345    {
346        $this->_url = &new Net_URL($url, $this->_useBrackets);
347
348        if (!empty($this->_url->user) || !empty($this->_url->pass)) {
349            $this->setBasicAuth($this->_url->user, $this->_url->pass);
350        }
351
352        if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
353            $this->addHeader('Host', $this->_generateHostHeader());
354        }
355
356        // set '/' instead of empty path rather than check later (see bug #8662)
357        if (empty($this->_url->path)) {
358            $this->_url->path = '/';
359        }
360    }
361   
362   /**
363    * Returns the current request URL 
364    *
365    * @return   string  Current request URL
366    * @access   public
367    */
368    function getUrl($url)
369    {
370        return empty($this->_url)? '': $this->_url->getUrl();
371    }
372
373    /**
374    * Sets a proxy to be used
375    *
376    * @param string     Proxy host
377    * @param int        Proxy port
378    * @param string     Proxy username
379    * @param string     Proxy password
380    * @access public
381    */
382    function setProxy($host, $port = 8080, $user = null, $pass = null)
383    {
384        $this->_proxy_host = $host;
385        $this->_proxy_port = $port;
386        $this->_proxy_user = $user;
387        $this->_proxy_pass = $pass;
388
389        if (!empty($user)) {
390            $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
391        }
392    }
393
394    /**
395    * Sets basic authentication parameters
396    *
397    * @param string     Username
398    * @param string     Password
399    */
400    function setBasicAuth($user, $pass)
401    {
402        $this->_user = $user;
403        $this->_pass = $pass;
404
405        $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
406    }
407
408    /**
409    * Sets the method to be used, GET, POST etc.
410    *
411    * @param string     Method to use. Use the defined constants for this
412    * @access public
413    */
414    function setMethod($method)
415    {
416        $this->_method = $method;
417    }
418
419    /**
420    * Sets the HTTP version to use, 1.0 or 1.1
421    *
422    * @param string     Version to use. Use the defined constants for this
423    * @access public
424    */
425    function setHttpVer($http)
426    {
427        $this->_http = $http;
428    }
429
430    /**
431    * Adds a request header
432    *
433    * @param string     Header name
434    * @param string     Header value
435    * @access public
436    */
437    function addHeader($name, $value)
438    {
439        $this->_requestHeaders[strtolower($name)] = $value;
440    }
441
442    /**
443    * Removes a request header
444    *
445    * @param string     Header name to remove
446    * @access public
447    */
448    function removeHeader($name)
449    {
450        if (isset($this->_requestHeaders[strtolower($name)])) {
451            unset($this->_requestHeaders[strtolower($name)]);
452        }
453    }
454
455    /**
456    * Adds a querystring parameter
457    *
458    * @param string     Querystring parameter name
459    * @param string     Querystring parameter value
460    * @param bool       Whether the value is already urlencoded or not, default = not
461    * @access public
462    */
463    function addQueryString($name, $value, $preencoded = false)
464    {
465        $this->_url->addQueryString($name, $value, $preencoded);
466    }   
467   
468    /**
469    * Sets the querystring to literally what you supply
470    *
471    * @param string     The querystring data. Should be of the format foo=bar&x=y etc
472    * @param bool       Whether data is already urlencoded or not, default = already encoded
473    * @access public
474    */
475    function addRawQueryString($querystring, $preencoded = true)
476    {
477        $this->_url->addRawQueryString($querystring, $preencoded);
478    }
479
480    /**
481    * Adds postdata items
482    *
483    * @param string     Post data name
484    * @param string     Post data value
485    * @param bool       Whether data is already urlencoded or not, default = not
486    * @access public
487    */
488    function addPostData($name, $value, $preencoded = false)
489    {
490        if ($preencoded) {
491            $this->_postData[$name] = $value;
492        } else {
493            $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value);
494        }
495    }
496   
497    function addPostDataArray($array, $preencoded = false)
498    {
499        foreach($array as $key => $val){
500            $this->addPostData($key, $val, $preencoded);
501        }
502    }   
503
504   /**
505    * Recursively applies the callback function to the value
506    *
507    * @param    mixed   Callback function
508    * @param    mixed   Value to process
509    * @access   private
510    * @return   mixed   Processed value
511    */
512    function _arrayMapRecursive($callback, $value)
513    {
514        if (!is_array($value)) {
515            return call_user_func($callback, $value);
516        } else {
517            $map = array();
518            foreach ($value as $k => $v) {
519                $map[$k] = $this->_arrayMapRecursive($callback, $v);
520            }
521            return $map;
522        }
523    }
524
525   /**
526    * Adds a file to upload
527    *
528    * This also changes content-type to 'multipart/form-data' for proper upload
529    *
530    * @access public
531    * @param  string    name of file-upload field
532    * @param  mixed     file name(s)
533    * @param  mixed     content-type(s) of file(s) being uploaded
534    * @return bool      true on success
535    * @throws PEAR_Error
536    */
537    function addFile($inputName, $fileName, $contentType = 'application/octet-stream')
538    {
539        if (!is_array($fileName) && !is_readable($fileName)) {
540            return PEAR::raiseError("File '{$fileName}' is not readable");
541        } elseif (is_array($fileName)) {
542            foreach ($fileName as $name) {
543                if (!is_readable($name)) {
544                    return PEAR::raiseError("File '{$name}' is not readable");
545                }
546            }
547        }
548        $this->addHeader('Content-Type', 'multipart/form-data');
549        $this->_postFiles[$inputName] = array(
550            'name' => $fileName,
551            'type' => $contentType
552        );
553        return true;
554    }
555
556    /**
557    * Adds raw postdata (DEPRECATED)
558    *
559    * @param string     The data
560    * @param bool       Whether data is preencoded or not, default = already encoded
561    * @access public
562    * @deprecated       deprecated since 1.3.0, method setBody() should be used instead
563    */
564    function addRawPostData($postdata, $preencoded = true)
565    {
566        $this->_body = $preencoded ? $postdata : urlencode($postdata);
567    }
568
569   /**
570    * Sets the request body (for POST, PUT and similar requests)
571    *
572    * @param    string  Request body
573    * @access   public
574    */
575    function setBody($body)
576    {
577        $this->_body = $body;
578    }
579
580    /**
581    * Clears any postdata that has been added (DEPRECATED).
582    *
583    * Useful for multiple request scenarios.
584    *
585    * @access public
586    * @deprecated deprecated since 1.2
587    */
588    function clearPostData()
589    {
590        $this->_postData = null;
591    }
592
593    /**
594    * Appends a cookie to "Cookie:" header
595    *
596    * @param string $name cookie name
597    * @param string $value cookie value
598    * @access public
599    */
600    function addCookie($name, $value)
601    {
602        $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : '';
603        $this->addHeader('Cookie', $cookies . $name . '=' . $value);
604    }
605   
606    /**
607    * Clears any cookies that have been added (DEPRECATED).
608    *
609    * Useful for multiple request scenarios
610    *
611    * @access public
612    * @deprecated deprecated since 1.2
613    */
614    function clearCookies()
615    {
616        $this->removeHeader('Cookie');
617    }
618
619    /**
620    * Sends the request
621    *
622    * @access public
623    * @param  bool   Whether to store response body in Response object property,
624    *                set this to false if downloading a LARGE file and using a Listener
625    * @return mixed  PEAR error on error, true otherwise
626    */
627    function sendRequest($saveBody = true)
628    {
629        if (!is_a($this->_url, 'Net_URL')) {
630            return PEAR::raiseError('No URL given.');
631        }
632
633        $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
634        $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;
635
636        // 4.3.0 supports SSL connections using OpenSSL. The function test determines
637        // we running on at least 4.3.0
638        if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) {
639            if (isset($this->_proxy_host)) {
640                return PEAR::raiseError('HTTPS proxies are not supported.');
641            }
642            $host = 'ssl://' . $host;
643        }
644
645        // magic quotes may fuck up file uploads and chunked response processing
646        $magicQuotes = ini_get('magic_quotes_runtime');
647        ini_set('magic_quotes_runtime', false);
648
649        // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
650        // connection token to a proxy server...
651        if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) &&
652            'Keep-Alive' == $this->_requestHeaders['connection'])
653        {
654            $this->removeHeader('connection');
655        }
656
657        $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) ||
658                     (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']);
659        $sockets   = &PEAR::getStaticProperty('HTTP_Request', 'sockets');
660        $sockKey   = $host . ':' . $port;
661        unset($this->_sock);
662
663       
664        sfprintr("socket");
665        // There is a connected socket in the "static" property?
666        if ($keepAlive && !empty($sockets[$sockKey]) &&
667            !empty($sockets[$sockKey]->fp))
668        {
669            $this->_sock =& $sockets[$sockKey];
670            $err = null;
671        } else {
672            $this->_notify('connect');
673            $this->_sock =& new Net_Socket();
674                    sfprintr($this->_sock);
675
676            $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
677        }
678        PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());
679
680        if (!PEAR::isError($err)) {
681            if (!empty($this->_readTimeout)) {
682                $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
683            }
684
685            $this->_notify('sentRequest');
686
687            // Read the response
688            $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
689            sfprintr($this->_response);
690            $err = $this->_response->process(
691                $this->_saveBody && $saveBody,
692                HTTP_REQUEST_METHOD_HEAD != $this->_method
693            );
694
695            if ($keepAlive) {
696                $keepAlive = (isset($this->_response->_headers['content-length'])
697                              || (isset($this->_response->_headers['transfer-encoding'])
698                                  && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked'));
699                if ($keepAlive) {
700                    if (isset($this->_response->_headers['connection'])) {
701                        $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';
702                    } else {
703                        $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;
704                    }
705                }
706            }
707        }
708
709        ini_set('magic_quotes_runtime', $magicQuotes);
710
711        if (PEAR::isError($err)) {
712            return $err;
713        }
714
715        if (!$keepAlive) {
716            $this->disconnect();
717        // Store the connected socket in "static" property
718        } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {
719            $sockets[$sockKey] =& $this->_sock;
720        }
721
722        // Check for redirection
723        if (    $this->_allowRedirects
724            AND $this->_redirects <= $this->_maxRedirects
725            AND $this->getResponseCode() > 300
726            AND $this->getResponseCode() < 399
727            AND !empty($this->_response->_headers['location'])) {
728
729           
730            $redirect = $this->_response->_headers['location'];
731
732            // Absolute URL
733            if (preg_match('/^https?:\/\//i', $redirect)) {
734                $this->_url = &new Net_URL($redirect);
735                $this->addHeader('Host', $this->_generateHostHeader());
736            // Absolute path
737            } elseif ($redirect{0} == '/') {
738                $this->_url->path = $redirect;
739           
740            // Relative path
741            } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
742                if (substr($this->_url->path, -1) == '/') {
743                    $redirect = $this->_url->path . $redirect;
744                } else {
745                    $redirect = dirname($this->_url->path) . '/' . $redirect;
746                }
747                $redirect = Net_URL::resolvePath($redirect);
748                $this->_url->path = $redirect;
749               
750            // Filename, no path
751            } else {
752                if (substr($this->_url->path, -1) == '/') {
753                    $redirect = $this->_url->path . $redirect;
754                } else {
755                    $redirect = dirname($this->_url->path) . '/' . $redirect;
756                }
757                $this->_url->path = $redirect;
758            }
759
760            $this->_redirects++;
761            return $this->sendRequest($saveBody);
762
763        // Too many redirects
764        } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
765            return PEAR::raiseError('Too many redirects');
766        }
767
768        return true;
769    }
770
771    /**
772     * Disconnect the socket, if connected. Only useful if using Keep-Alive.
773     *
774     * @access public
775     */
776    function disconnect()
777    {
778        if (!empty($this->_sock) && !empty($this->_sock->fp)) {
779            $this->_notify('disconnect');
780            $this->_sock->disconnect();
781        }
782    }
783
784    /**
785    * Returns the response code
786    *
787    * @access public
788    * @return mixed     Response code, false if not set
789    */
790    function getResponseCode()
791    {
792        return isset($this->_response->_code) ? $this->_response->_code : false;
793    }
794
795    /**
796    * Returns either the named header or all if no name given
797    *
798    * @access public
799    * @param string     The header name to return, do not set to get all headers
800    * @return mixed     either the value of $headername (false if header is not present)
801    *                   or an array of all headers
802    */
803    function getResponseHeader($headername = null)
804    {
805        if (!isset($headername)) {
806            return isset($this->_response->_headers)? $this->_response->_headers: array();
807        } else {
808            $headername = strtolower($headername);
809            return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
810        }
811    }
812
813    /**
814    * Returns the body of the response
815    *
816    * @access public
817    * @return mixed     response body, false if not set
818    */
819    function getResponseBody()
820    {
821        return isset($this->_response->_body) ? $this->_response->_body : false;
822    }
823
824    /**
825    * Returns cookies set in response
826    *
827    * @access public
828    * @return mixed     array of response cookies, false if none are present
829    */
830    function getResponseCookies()
831    {
832        return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
833    }
834
835    /**
836    * Builds the request string
837    *
838    * @access private
839    * @return string The request string
840    */
841    function _buildRequest()
842    {
843        $separator = ini_get('arg_separator.output');
844        ini_set('arg_separator.output', '&');
845        $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
846        ini_set('arg_separator.output', $separator);
847
848        $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
849        $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
850        $path = $this->_url->path . $querystring;
851        $url  = $host . $port . $path;
852
853        $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
854
855        if (in_array($this->_method, $this->_bodyDisallowed) ||
856            (empty($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method ||
857             (empty($this->_postData) && empty($this->_postFiles)))))
858        {
859            $this->removeHeader('Content-Type');
860        } else {
861            if (empty($this->_requestHeaders['content-type'])) {
862                // Add default content-type
863                $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
864            } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
865                $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
866                $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
867            }
868        }
869
870        // Request Headers
871        if (!empty($this->_requestHeaders)) {
872            foreach ($this->_requestHeaders as $name => $value) {
873                $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
874                $request      .= $canonicalName . ': ' . $value . "\r\n";
875            }
876        }
877
878        // No post data or wrong method, so simply add a final CRLF
879        if (in_array($this->_method, $this->_bodyDisallowed) ||
880            (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) {
881
882            $request .= "\r\n";
883
884        // Post data if it's an array
885        } elseif (HTTP_REQUEST_METHOD_POST == $this->_method &&
886                  (!empty($this->_postData) || !empty($this->_postFiles))) {
887
888            // "normal" POST request
889            if (!isset($boundary)) {
890                $postdata = implode('&', array_map(
891                    create_function('$a', 'return $a[0] . \'=\' . $a[1];'),
892                    $this->_flattenArray('', $this->_postData)
893                ));
894
895            // multipart request, probably with file uploads
896            } else {
897                $postdata = '';
898                if (!empty($this->_postData)) {
899                    $flatData = $this->_flattenArray('', $this->_postData);
900                    foreach ($flatData as $item) {
901                        $postdata .= '--' . $boundary . "\r\n";
902                        $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
903                        $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
904                    }
905                }
906                foreach ($this->_postFiles as $name => $value) {
907                    if (is_array($value['name'])) {
908                        $varname       = $name . ($this->_useBrackets? '[]': '');
909                    } else {
910                        $varname       = $name;
911                        $value['name'] = array($value['name']);
912                    }
913                    foreach ($value['name'] as $key => $filename) {
914                        $fp   = fopen($filename, 'r');
915                        $data = fread($fp, filesize($filename));
916                        fclose($fp);
917                        $basename = basename($filename);
918                        $type     = is_array($value['type'])? @$value['type'][$key]: $value['type'];
919
920                        $postdata .= '--' . $boundary . "\r\n";
921                        $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
922                        $postdata .= "\r\nContent-Type: " . $type;
923                        $postdata .= "\r\n\r\n" . $data . "\r\n";
924                    }
925                }
926                $postdata .= '--' . $boundary . "--\r\n";
927            }
928            $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n";
929            $request .= $postdata;
930
931        // Explicitly set request body
932        } elseif (!empty($this->_body)) {
933
934            $request .= 'Content-Length: ' . strlen($this->_body) . "\r\n\r\n";
935            $request .= $this->_body;
936        }
937       
938        return $request;
939    }
940
941   /**
942    * Helper function to change the (probably multidimensional) associative array
943    * into the simple one.
944    *
945    * @param    string  name for item
946    * @param    mixed   item's values
947    * @return   array   array with the following items: array('item name', 'item value');
948    */
949    function _flattenArray($name, $values)
950    {
951        if (!is_array($values)) {
952            return array(array($name, $values));
953        } else {
954            $ret = array();
955            foreach ($values as $k => $v) {
956                if (empty($name)) {
957                    $newName = $k;
958                } elseif ($this->_useBrackets) {
959                    $newName = $name . '[' . $k . ']';
960                } else {
961                    $newName = $name;
962                }
963                $ret = array_merge($ret, $this->_flattenArray($newName, $v));
964            }
965            return $ret;
966        }
967    }
968
969
970   /**
971    * Adds a Listener to the list of listeners that are notified of
972    * the object's events
973    *
974    * @param    object   HTTP_Request_Listener instance to attach
975    * @return   boolean  whether the listener was successfully attached
976    * @access   public
977    */
978    function attach(&$listener)
979    {
980        if (!is_a($listener, 'HTTP_Request_Listener')) {
981            return false;
982        }
983        $this->_listeners[$listener->getId()] =& $listener;
984        return true;
985    }
986
987
988   /**
989    * Removes a Listener from the list of listeners
990    *
991    * @param    object   HTTP_Request_Listener instance to detach
992    * @return   boolean  whether the listener was successfully detached
993    * @access   public
994    */
995    function detach(&$listener)
996    {
997        if (!is_a($listener, 'HTTP_Request_Listener') ||
998            !isset($this->_listeners[$listener->getId()])) {
999            return false;
1000        }
1001        unset($this->_listeners[$listener->getId()]);
1002        return true;
1003    }
1004
1005
1006   /**
1007    * Notifies all registered listeners of an event.
1008    *
1009    * Events sent by HTTP_Request object
1010    * - 'connect': on connection to server
1011    * - 'sentRequest': after the request was sent
1012    * - 'disconnect': on disconnection from server
1013    *
1014    * Events sent by HTTP_Response object
1015    * - 'gotHeaders': after receiving response headers (headers are passed in $data)
1016    * - 'tick': on receiving a part of response body (the part is passed in $data)
1017    * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)
1018    * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
1019    *
1020    * @param    string  Event name
1021    * @param    mixed   Additional data
1022    * @access   private
1023    */
1024    function _notify($event, $data = null)
1025    {
1026        foreach (array_keys($this->_listeners) as $id) {
1027            $this->_listeners[$id]->update($this, $event, $data);
1028        }
1029    }
1030}
1031
1032
1033/**
1034* Response class to complement the Request class
1035*/
1036class HTTP_Response
1037{
1038    /**
1039    * Socket object
1040    * @var object
1041    */
1042    var $_sock;
1043
1044    /**
1045    * Protocol
1046    * @var string
1047    */
1048    var $_protocol;
1049   
1050    /**
1051    * Return code
1052    * @var string
1053    */
1054    var $_code;
1055   
1056    /**
1057    * Response headers
1058    * @var array
1059    */
1060    var $_headers;
1061
1062    /**
1063    * Cookies set in response 
1064    * @var array
1065    */
1066    var $_cookies;
1067
1068    /**
1069    * Response body
1070    * @var string
1071    */
1072    var $_body = '';
1073
1074   /**
1075    * Used by _readChunked(): remaining length of the current chunk
1076    * @var string
1077    */
1078    var $_chunkLength = 0;
1079
1080   /**
1081    * Attached listeners
1082    * @var array
1083    */
1084    var $_listeners = array();
1085
1086   /**
1087    * Bytes left to read from message-body
1088    * @var null|int
1089    */
1090    var $_toRead;
1091
1092    /**
1093    * Constructor
1094    *
1095    * @param  object Net_Socket     socket to read the response from
1096    * @param  array                 listeners attached to request
1097    * @return mixed PEAR Error on error, true otherwise
1098    */
1099    function HTTP_Response(&$sock, &$listeners)
1100    {
1101        $this->_sock      =& $sock;
1102        $this->_listeners =& $listeners;
1103    }
1104
1105
1106   /**
1107    * Processes a HTTP response
1108    *
1109    * This extracts response code, headers, cookies and decodes body if it
1110    * was encoded in some way
1111    *
1112    * @access public
1113    * @param  bool      Whether to store response body in object property, set
1114    *                   this to false if downloading a LARGE file and using a Listener.
1115    *                   This is assumed to be true if body is gzip-encoded.
1116    * @param  bool      Whether the response can actually have a message-body.
1117    *                   Will be set to false for HEAD requests.
1118    * @throws PEAR_Error
1119    * @return mixed     true on success, PEAR_Error in case of malformed response
1120    */
1121    function process($saveBody = true, $canHaveBody = true)
1122    {
1123        do {
1124            $line = $this->_sock->readLine();
1125            if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
1126                return PEAR::raiseError('Malformed response.');
1127            } else {
1128                $this->_protocol = 'HTTP/' . $http_version;
1129                $this->_code     = intval($returncode);
1130            }
1131            while ('' !== ($header = $this->_sock->readLine())) {
1132                $this->_processHeader($header);
1133            }
1134        } while (100 == $this->_code);
1135
1136        $this->_notify('gotHeaders', $this->_headers);
1137
1138        // RFC 2616, section 4.4:
1139        // 1. Any response message which "MUST NOT" include a message-body ...
1140        // is always terminated by the first empty line after the header fields
1141        // 3. ... If a message is received with both a
1142        // Transfer-Encoding header field and a Content-Length header field,
1143        // the latter MUST be ignored.
1144        $canHaveBody = $canHaveBody && $this->_code >= 200 &&
1145                       $this->_code != 204 && $this->_code != 304;
1146
1147        // If response body is present, read it and decode
1148        $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
1149        $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
1150        $hasBody = false;
1151        if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) ||
1152                0 != $this->_headers['content-length']))
1153        {
1154            if ($chunked || !isset($this->_headers['content-length'])) {
1155                $this->_toRead = null;
1156            } else {
1157                $this->_toRead = $this->_headers['content-length'];
1158            }
1159            while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {
1160                if ($chunked) {
1161                    $data = $this->_readChunked();
1162                } elseif (is_null($this->_toRead)) {
1163                    $data = $this->_sock->read(4096);
1164                } else {
1165                    $data = $this->_sock->read(min(4096, $this->_toRead));
1166                    $this->_toRead -= strlen($data);
1167                }
1168                if ('' == $data) {
1169                    break;
1170                } else {
1171                    $hasBody = true;
1172                    if ($saveBody || $gzipped) {
1173                        $this->_body .= $data;
1174                    }
1175                    $this->_notify($gzipped? 'gzTick': 'tick', $data);
1176                }
1177            }
1178        }
1179
1180        if ($hasBody) {
1181            // Uncompress the body if needed
1182            if ($gzipped) {
1183                $body = $this->_decodeGzip($this->_body);
1184                if (PEAR::isError($body)) {
1185                    return $body;
1186                }
1187                $this->_body = $body;
1188                $this->_notify('gotBody', $this->_body);
1189            } else {
1190                $this->_notify('gotBody');
1191            }
1192        }
1193        return true;
1194    }
1195
1196
1197   /**
1198    * Processes the response header
1199    *
1200    * @access private
1201    * @param  string    HTTP header
1202    */
1203    function _processHeader($header)
1204    {
1205        if (false === strpos($header, ':')) {
1206            return;
1207        }
1208        list($headername, $headervalue) = explode(':', $header, 2);
1209        $headername  = strtolower($headername);
1210        $headervalue = ltrim($headervalue);
1211       
1212        if ('set-cookie' != $headername) {
1213            if (isset($this->_headers[$headername])) {
1214                $this->_headers[$headername] .= ',' . $headervalue;
1215            } else {
1216                $this->_headers[$headername]  = $headervalue;
1217            }
1218        } else {
1219            $this->_parseCookie($headervalue);
1220        }
1221    }
1222
1223
1224   /**
1225    * Parse a Set-Cookie header to fill $_cookies array
1226    *
1227    * @access private
1228    * @param  string    value of Set-Cookie header
1229    */
1230    function _parseCookie($headervalue)
1231    {
1232        $cookie = array(
1233            'expires' => null,
1234            'domain'  => null,
1235            'path'    => null,
1236            'secure'  => false
1237        );
1238
1239        // Only a name=value pair
1240        if (!strpos($headervalue, ';')) {
1241            $pos = strpos($headervalue, '=');
1242            $cookie['name']  = trim(substr($headervalue, 0, $pos));
1243            $cookie['value'] = trim(substr($headervalue, $pos + 1));
1244
1245        // Some optional parameters are supplied
1246        } else {
1247            $elements = explode(';', $headervalue);
1248            $pos = strpos($elements[0], '=');
1249            $cookie['name']  = trim(substr($elements[0], 0, $pos));
1250            $cookie['value'] = trim(substr($elements[0], $pos + 1));
1251
1252            for ($i = 1; $i < count($elements); $i++) {
1253                if (false === strpos($elements[$i], '=')) {
1254                    $elName  = trim($elements[$i]);
1255                    $elValue = null;
1256                } else {
1257                    list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
1258                }
1259                $elName = strtolower($elName);
1260                if ('secure' == $elName) {
1261                    $cookie['secure'] = true;
1262                } elseif ('expires' == $elName) {
1263                    $cookie['expires'] = str_replace('"', '', $elValue);
1264                } elseif ('path' == $elName || 'domain' == $elName) {
1265                    $cookie[$elName] = urldecode($elValue);
1266                } else {
1267                    $cookie[$elName] = $elValue;
1268                }
1269            }
1270        }
1271        $this->_cookies[] = $cookie;
1272    }
1273
1274
1275   /**
1276    * Read a part of response body encoded with chunked Transfer-Encoding
1277    *
1278    * @access private
1279    * @return string
1280    */
1281    function _readChunked()
1282    {
1283        // at start of the next chunk?
1284        if (0 == $this->_chunkLength) {
1285            $line = $this->_sock->readLine();
1286            if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
1287                $this->_chunkLength = hexdec($matches[1]);
1288                // Chunk with zero length indicates the end
1289                if (0 == $this->_chunkLength) {
1290                    $this->_sock->readLine(); // make this an eof()
1291                    return '';
1292                }
1293            } else {
1294                return '';
1295            }
1296        }
1297        $data = $this->_sock->read($this->_chunkLength);
1298        $this->_chunkLength -= strlen($data);
1299        if (0 == $this->_chunkLength) {
1300            $this->_sock->readLine(); // Trailing CRLF
1301        }
1302        return $data;
1303    }
1304
1305
1306   /**
1307    * Notifies all registered listeners of an event.
1308    *
1309    * @param    string  Event name
1310    * @param    mixed   Additional data
1311    * @access   private
1312    * @see HTTP_Request::_notify()
1313    */
1314    function _notify($event, $data = null)
1315    {
1316        foreach (array_keys($this->_listeners) as $id) {
1317            $this->_listeners[$id]->update($this, $event, $data);
1318        }
1319    }
1320
1321
1322   /**
1323    * Decodes the message-body encoded by gzip
1324    *
1325    * The real decoding work is done by gzinflate() built-in function, this
1326    * method only parses the header and checks data for compliance with
1327    * RFC 1952 
1328    *
1329    * @access   private
1330    * @param    string  gzip-encoded data
1331    * @return   string  decoded data
1332    */
1333    function _decodeGzip($data)
1334    {
1335        $length = strlen($data);
1336        // If it doesn't look like gzip-encoded data, don't bother
1337        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
1338            return $data;
1339        }
1340        $method = ord(substr($data, 2, 1));
1341        if (8 != $method) {
1342            return PEAR::raiseError('_decodeGzip(): unknown compression method');
1343        }
1344        $flags = ord(substr($data, 3, 1));
1345        if ($flags & 224) {
1346            return PEAR::raiseError('_decodeGzip(): reserved bits are set');
1347        }
1348
1349        // header is 10 bytes minimum. may be longer, though.
1350        $headerLength = 10;
1351        // extra fields, need to skip 'em
1352        if ($flags & 4) {
1353            if ($length - $headerLength - 2 < 8) {
1354                return PEAR::raiseError('_decodeGzip(): data too short');
1355            }
1356            $extraLength = unpack('v', substr($data, 10, 2));
1357            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
1358                return PEAR::raiseError('_decodeGzip(): data too short');
1359            }
1360            $headerLength += $extraLength[1] + 2;
1361        }
1362        // file name, need to skip that
1363        if ($flags & 8) {
1364            if ($length - $headerLength - 1 < 8) {
1365                return PEAR::raiseError('_decodeGzip(): data too short');
1366            }
1367            $filenameLength = strpos(substr($data, $headerLength), chr(0));
1368            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
1369                return PEAR::raiseError('_decodeGzip(): data too short');
1370            }
1371            $headerLength += $filenameLength + 1;
1372        }
1373        // comment, need to skip that also
1374        if ($flags & 16) {
1375            if ($length - $headerLength - 1 < 8) {
1376                return PEAR::raiseError('_decodeGzip(): data too short');
1377            }
1378            $commentLength = strpos(substr($data, $headerLength), chr(0));
1379            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
1380                return PEAR::raiseError('_decodeGzip(): data too short');
1381            }
1382            $headerLength += $commentLength + 1;
1383        }
1384        // have a CRC for header. let's check
1385        if ($flags & 1) {
1386            if ($length - $headerLength - 2 < 8) {
1387                return PEAR::raiseError('_decodeGzip(): data too short');
1388            }
1389            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
1390            $crcStored = unpack('v', substr($data, $headerLength, 2));
1391            if ($crcReal != $crcStored[1]) {
1392                return PEAR::raiseError('_decodeGzip(): header CRC check failed');
1393            }
1394            $headerLength += 2;
1395        }
1396        // unpacked data CRC and size at the end of encoded data
1397        $tmp = unpack('V2', substr($data, -8));
1398        $dataCrc  = $tmp[1];
1399        $dataSize = $tmp[2];
1400
1401        // finally, call the gzinflate() function
1402        $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize);
1403        if (false === $unpacked) {
1404            return PEAR::raiseError('_decodeGzip(): gzinflate() call failed');
1405        } elseif ($dataSize != strlen($unpacked)) {
1406            return PEAR::raiseError('_decodeGzip(): data size check failed');
1407        } elseif ($dataCrc != crc32($unpacked)) {
1408            return PEAR::raiseError('_decodeGzip(): data CRC check failed');
1409        }
1410        return $unpacked;
1411    }
1412} // End class HTTP_Response
1413?>
Note: See TracBrowser for help on using the repository browser.