source: branches/comu/data/module/Request.php @ 2

Revision 2, 45.0 KB checked in by root, 17 years ago (diff)

new import

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 <richard@phpguru.org>                           |
33// +-----------------------------------------------------------------------+
34//
35// $Id: Request.php 10017 2006-12-27 02:29:54Z kaki $
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        // There is a connected socket in the "static" property?
664        if ($keepAlive && !empty($sockets[$sockKey]) &&
665            !empty($sockets[$sockKey]->fp))
666        {
667            $this->_sock =& $sockets[$sockKey];
668            $err = null;
669        } else {
670            $this->_notify('connect');
671            $this->_sock =& new Net_Socket();
672            $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
673        }
674        PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());
675
676        if (!PEAR::isError($err)) {
677            if (!empty($this->_readTimeout)) {
678                $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
679            }
680
681            $this->_notify('sentRequest');
682
683            // Read the response
684            $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
685            $err = $this->_response->process(
686                $this->_saveBody && $saveBody,
687                HTTP_REQUEST_METHOD_HEAD != $this->_method
688            );
689
690            if ($keepAlive) {
691                $keepAlive = (isset($this->_response->_headers['content-length'])
692                              || (isset($this->_response->_headers['transfer-encoding'])
693                                  && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked'));
694                if ($keepAlive) {
695                    if (isset($this->_response->_headers['connection'])) {
696                        $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';
697                    } else {
698                        $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;
699                    }
700                }
701            }
702        }
703
704        ini_set('magic_quotes_runtime', $magicQuotes);
705
706        if (PEAR::isError($err)) {
707            return $err;
708        }
709
710        if (!$keepAlive) {
711            $this->disconnect();
712        // Store the connected socket in "static" property
713        } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {
714            $sockets[$sockKey] =& $this->_sock;
715        }
716
717        // Check for redirection
718        if (    $this->_allowRedirects
719            AND $this->_redirects <= $this->_maxRedirects
720            AND $this->getResponseCode() > 300
721            AND $this->getResponseCode() < 399
722            AND !empty($this->_response->_headers['location'])) {
723
724           
725            $redirect = $this->_response->_headers['location'];
726
727            // Absolute URL
728            if (preg_match('/^https?:\/\//i', $redirect)) {
729                $this->_url = &new Net_URL($redirect);
730                $this->addHeader('Host', $this->_generateHostHeader());
731            // Absolute path
732            } elseif ($redirect{0} == '/') {
733                $this->_url->path = $redirect;
734           
735            // Relative path
736            } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
737                if (substr($this->_url->path, -1) == '/') {
738                    $redirect = $this->_url->path . $redirect;
739                } else {
740                    $redirect = dirname($this->_url->path) . '/' . $redirect;
741                }
742                $redirect = Net_URL::resolvePath($redirect);
743                $this->_url->path = $redirect;
744               
745            // Filename, no path
746            } else {
747                if (substr($this->_url->path, -1) == '/') {
748                    $redirect = $this->_url->path . $redirect;
749                } else {
750                    $redirect = dirname($this->_url->path) . '/' . $redirect;
751                }
752                $this->_url->path = $redirect;
753            }
754
755            $this->_redirects++;
756            return $this->sendRequest($saveBody);
757
758        // Too many redirects
759        } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
760            return PEAR::raiseError('Too many redirects');
761        }
762
763        return true;
764    }
765
766    /**
767     * Disconnect the socket, if connected. Only useful if using Keep-Alive.
768     *
769     * @access public
770     */
771    function disconnect()
772    {
773        if (!empty($this->_sock) && !empty($this->_sock->fp)) {
774            $this->_notify('disconnect');
775            $this->_sock->disconnect();
776        }
777    }
778
779    /**
780    * Returns the response code
781    *
782    * @access public
783    * @return mixed     Response code, false if not set
784    */
785    function getResponseCode()
786    {
787        return isset($this->_response->_code) ? $this->_response->_code : false;
788    }
789
790    /**
791    * Returns either the named header or all if no name given
792    *
793    * @access public
794    * @param string     The header name to return, do not set to get all headers
795    * @return mixed     either the value of $headername (false if header is not present)
796    *                   or an array of all headers
797    */
798    function getResponseHeader($headername = null)
799    {
800        if (!isset($headername)) {
801            return isset($this->_response->_headers)? $this->_response->_headers: array();
802        } else {
803            $headername = strtolower($headername);
804            return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
805        }
806    }
807
808    /**
809    * Returns the body of the response
810    *
811    * @access public
812    * @return mixed     response body, false if not set
813    */
814    function getResponseBody()
815    {
816        return isset($this->_response->_body) ? $this->_response->_body : false;
817    }
818
819    /**
820    * Returns cookies set in response
821    *
822    * @access public
823    * @return mixed     array of response cookies, false if none are present
824    */
825    function getResponseCookies()
826    {
827        return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
828    }
829
830    /**
831    * Builds the request string
832    *
833    * @access private
834    * @return string The request string
835    */
836    function _buildRequest()
837    {
838        $separator = ini_get('arg_separator.output');
839        ini_set('arg_separator.output', '&');
840        $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
841        ini_set('arg_separator.output', $separator);
842
843        $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
844        $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
845        $path = $this->_url->path . $querystring;
846        $url  = $host . $port . $path;
847
848        $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
849
850        if (in_array($this->_method, $this->_bodyDisallowed) ||
851            (empty($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method ||
852             (empty($this->_postData) && empty($this->_postFiles)))))
853        {
854            $this->removeHeader('Content-Type');
855        } else {
856            if (empty($this->_requestHeaders['content-type'])) {
857                // Add default content-type
858                $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
859            } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
860                $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
861                $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
862            }
863        }
864
865        // Request Headers
866        if (!empty($this->_requestHeaders)) {
867            foreach ($this->_requestHeaders as $name => $value) {
868                $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
869                $request      .= $canonicalName . ': ' . $value . "\r\n";
870            }
871        }
872
873        // No post data or wrong method, so simply add a final CRLF
874        if (in_array($this->_method, $this->_bodyDisallowed) ||
875            (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) {
876
877            $request .= "\r\n";
878
879        // Post data if it's an array
880        } elseif (HTTP_REQUEST_METHOD_POST == $this->_method &&
881                  (!empty($this->_postData) || !empty($this->_postFiles))) {
882
883            // "normal" POST request
884            if (!isset($boundary)) {
885                $postdata = implode('&', array_map(
886                    create_function('$a', 'return $a[0] . \'=\' . $a[1];'),
887                    $this->_flattenArray('', $this->_postData)
888                ));
889
890            // multipart request, probably with file uploads
891            } else {
892                $postdata = '';
893                if (!empty($this->_postData)) {
894                    $flatData = $this->_flattenArray('', $this->_postData);
895                    foreach ($flatData as $item) {
896                        $postdata .= '--' . $boundary . "\r\n";
897                        $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
898                        $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
899                    }
900                }
901                foreach ($this->_postFiles as $name => $value) {
902                    if (is_array($value['name'])) {
903                        $varname       = $name . ($this->_useBrackets? '[]': '');
904                    } else {
905                        $varname       = $name;
906                        $value['name'] = array($value['name']);
907                    }
908                    foreach ($value['name'] as $key => $filename) {
909                        $fp   = fopen($filename, 'r');
910                        $data = fread($fp, filesize($filename));
911                        fclose($fp);
912                        $basename = basename($filename);
913                        $type     = is_array($value['type'])? @$value['type'][$key]: $value['type'];
914
915                        $postdata .= '--' . $boundary . "\r\n";
916                        $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
917                        $postdata .= "\r\nContent-Type: " . $type;
918                        $postdata .= "\r\n\r\n" . $data . "\r\n";
919                    }
920                }
921                $postdata .= '--' . $boundary . "--\r\n";
922            }
923            $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n";
924            $request .= $postdata;
925
926        // Explicitly set request body
927        } elseif (!empty($this->_body)) {
928
929            $request .= 'Content-Length: ' . strlen($this->_body) . "\r\n\r\n";
930            $request .= $this->_body;
931        }
932       
933        return $request;
934    }
935
936   /**
937    * Helper function to change the (probably multidimensional) associative array
938    * into the simple one.
939    *
940    * @param    string  name for item
941    * @param    mixed   item's values
942    * @return   array   array with the following items: array('item name', 'item value');
943    */
944    function _flattenArray($name, $values)
945    {
946        if (!is_array($values)) {
947            return array(array($name, $values));
948        } else {
949            $ret = array();
950            foreach ($values as $k => $v) {
951                if (empty($name)) {
952                    $newName = $k;
953                } elseif ($this->_useBrackets) {
954                    $newName = $name . '[' . $k . ']';
955                } else {
956                    $newName = $name;
957                }
958                $ret = array_merge($ret, $this->_flattenArray($newName, $v));
959            }
960            return $ret;
961        }
962    }
963
964
965   /**
966    * Adds a Listener to the list of listeners that are notified of
967    * the object's events
968    *
969    * @param    object   HTTP_Request_Listener instance to attach
970    * @return   boolean  whether the listener was successfully attached
971    * @access   public
972    */
973    function attach(&$listener)
974    {
975        if (!is_a($listener, 'HTTP_Request_Listener')) {
976            return false;
977        }
978        $this->_listeners[$listener->getId()] =& $listener;
979        return true;
980    }
981
982
983   /**
984    * Removes a Listener from the list of listeners
985    *
986    * @param    object   HTTP_Request_Listener instance to detach
987    * @return   boolean  whether the listener was successfully detached
988    * @access   public
989    */
990    function detach(&$listener)
991    {
992        if (!is_a($listener, 'HTTP_Request_Listener') ||
993            !isset($this->_listeners[$listener->getId()])) {
994            return false;
995        }
996        unset($this->_listeners[$listener->getId()]);
997        return true;
998    }
999
1000
1001   /**
1002    * Notifies all registered listeners of an event.
1003    *
1004    * Events sent by HTTP_Request object
1005    * - 'connect': on connection to server
1006    * - 'sentRequest': after the request was sent
1007    * - 'disconnect': on disconnection from server
1008    *
1009    * Events sent by HTTP_Response object
1010    * - 'gotHeaders': after receiving response headers (headers are passed in $data)
1011    * - 'tick': on receiving a part of response body (the part is passed in $data)
1012    * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)
1013    * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
1014    *
1015    * @param    string  Event name
1016    * @param    mixed   Additional data
1017    * @access   private
1018    */
1019    function _notify($event, $data = null)
1020    {
1021        foreach (array_keys($this->_listeners) as $id) {
1022            $this->_listeners[$id]->update($this, $event, $data);
1023        }
1024    }
1025}
1026
1027
1028/**
1029* Response class to complement the Request class
1030*/
1031class HTTP_Response
1032{
1033    /**
1034    * Socket object
1035    * @var object
1036    */
1037    var $_sock;
1038
1039    /**
1040    * Protocol
1041    * @var string
1042    */
1043    var $_protocol;
1044   
1045    /**
1046    * Return code
1047    * @var string
1048    */
1049    var $_code;
1050   
1051    /**
1052    * Response headers
1053    * @var array
1054    */
1055    var $_headers;
1056
1057    /**
1058    * Cookies set in response 
1059    * @var array
1060    */
1061    var $_cookies;
1062
1063    /**
1064    * Response body
1065    * @var string
1066    */
1067    var $_body = '';
1068
1069   /**
1070    * Used by _readChunked(): remaining length of the current chunk
1071    * @var string
1072    */
1073    var $_chunkLength = 0;
1074
1075   /**
1076    * Attached listeners
1077    * @var array
1078    */
1079    var $_listeners = array();
1080
1081   /**
1082    * Bytes left to read from message-body
1083    * @var null|int
1084    */
1085    var $_toRead;
1086
1087    /**
1088    * Constructor
1089    *
1090    * @param  object Net_Socket     socket to read the response from
1091    * @param  array                 listeners attached to request
1092    * @return mixed PEAR Error on error, true otherwise
1093    */
1094    function HTTP_Response(&$sock, &$listeners)
1095    {
1096        $this->_sock      =& $sock;
1097        $this->_listeners =& $listeners;
1098    }
1099
1100
1101   /**
1102    * Processes a HTTP response
1103    *
1104    * This extracts response code, headers, cookies and decodes body if it
1105    * was encoded in some way
1106    *
1107    * @access public
1108    * @param  bool      Whether to store response body in object property, set
1109    *                   this to false if downloading a LARGE file and using a Listener.
1110    *                   This is assumed to be true if body is gzip-encoded.
1111    * @param  bool      Whether the response can actually have a message-body.
1112    *                   Will be set to false for HEAD requests.
1113    * @throws PEAR_Error
1114    * @return mixed     true on success, PEAR_Error in case of malformed response
1115    */
1116    function process($saveBody = true, $canHaveBody = true)
1117    {
1118        do {
1119            $line = $this->_sock->readLine();
1120            if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
1121                return PEAR::raiseError('Malformed response.');
1122            } else {
1123                $this->_protocol = 'HTTP/' . $http_version;
1124                $this->_code     = intval($returncode);
1125            }
1126            while ('' !== ($header = $this->_sock->readLine())) {
1127                $this->_processHeader($header);
1128            }
1129        } while (100 == $this->_code);
1130
1131        $this->_notify('gotHeaders', $this->_headers);
1132
1133        // RFC 2616, section 4.4:
1134        // 1. Any response message which "MUST NOT" include a message-body ...
1135        // is always terminated by the first empty line after the header fields
1136        // 3. ... If a message is received with both a
1137        // Transfer-Encoding header field and a Content-Length header field,
1138        // the latter MUST be ignored.
1139        $canHaveBody = $canHaveBody && $this->_code >= 200 &&
1140                       $this->_code != 204 && $this->_code != 304;
1141
1142        // If response body is present, read it and decode
1143        $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
1144        $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
1145        $hasBody = false;
1146        if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) ||
1147                0 != $this->_headers['content-length']))
1148        {
1149            if ($chunked || !isset($this->_headers['content-length'])) {
1150                $this->_toRead = null;
1151            } else {
1152                $this->_toRead = $this->_headers['content-length'];
1153            }
1154            while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {
1155                if ($chunked) {
1156                    $data = $this->_readChunked();
1157                } elseif (is_null($this->_toRead)) {
1158                    $data = $this->_sock->read(4096);
1159                } else {
1160                    $data = $this->_sock->read(min(4096, $this->_toRead));
1161                    $this->_toRead -= strlen($data);
1162                }
1163                if ('' == $data) {
1164                    break;
1165                } else {
1166                    $hasBody = true;
1167                    if ($saveBody || $gzipped) {
1168                        $this->_body .= $data;
1169                    }
1170                    $this->_notify($gzipped? 'gzTick': 'tick', $data);
1171                }
1172            }
1173        }
1174
1175        if ($hasBody) {
1176            // Uncompress the body if needed
1177            if ($gzipped) {
1178                $body = $this->_decodeGzip($this->_body);
1179                if (PEAR::isError($body)) {
1180                    return $body;
1181                }
1182                $this->_body = $body;
1183                $this->_notify('gotBody', $this->_body);
1184            } else {
1185                $this->_notify('gotBody');
1186            }
1187        }
1188        return true;
1189    }
1190
1191
1192   /**
1193    * Processes the response header
1194    *
1195    * @access private
1196    * @param  string    HTTP header
1197    */
1198    function _processHeader($header)
1199    {
1200        if (false === strpos($header, ':')) {
1201            return;
1202        }
1203        list($headername, $headervalue) = explode(':', $header, 2);
1204        $headername  = strtolower($headername);
1205        $headervalue = ltrim($headervalue);
1206       
1207        if ('set-cookie' != $headername) {
1208            if (isset($this->_headers[$headername])) {
1209                $this->_headers[$headername] .= ',' . $headervalue;
1210            } else {
1211                $this->_headers[$headername]  = $headervalue;
1212            }
1213        } else {
1214            $this->_parseCookie($headervalue);
1215        }
1216    }
1217
1218
1219   /**
1220    * Parse a Set-Cookie header to fill $_cookies array
1221    *
1222    * @access private
1223    * @param  string    value of Set-Cookie header
1224    */
1225    function _parseCookie($headervalue)
1226    {
1227        $cookie = array(
1228            'expires' => null,
1229            'domain'  => null,
1230            'path'    => null,
1231            'secure'  => false
1232        );
1233
1234        // Only a name=value pair
1235        if (!strpos($headervalue, ';')) {
1236            $pos = strpos($headervalue, '=');
1237            $cookie['name']  = trim(substr($headervalue, 0, $pos));
1238            $cookie['value'] = trim(substr($headervalue, $pos + 1));
1239
1240        // Some optional parameters are supplied
1241        } else {
1242            $elements = explode(';', $headervalue);
1243            $pos = strpos($elements[0], '=');
1244            $cookie['name']  = trim(substr($elements[0], 0, $pos));
1245            $cookie['value'] = trim(substr($elements[0], $pos + 1));
1246
1247            for ($i = 1; $i < count($elements); $i++) {
1248                if (false === strpos($elements[$i], '=')) {
1249                    $elName  = trim($elements[$i]);
1250                    $elValue = null;
1251                } else {
1252                    list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
1253                }
1254                $elName = strtolower($elName);
1255                if ('secure' == $elName) {
1256                    $cookie['secure'] = true;
1257                } elseif ('expires' == $elName) {
1258                    $cookie['expires'] = str_replace('"', '', $elValue);
1259                } elseif ('path' == $elName || 'domain' == $elName) {
1260                    $cookie[$elName] = urldecode($elValue);
1261                } else {
1262                    $cookie[$elName] = $elValue;
1263                }
1264            }
1265        }
1266        $this->_cookies[] = $cookie;
1267    }
1268
1269
1270   /**
1271    * Read a part of response body encoded with chunked Transfer-Encoding
1272    *
1273    * @access private
1274    * @return string
1275    */
1276    function _readChunked()
1277    {
1278        // at start of the next chunk?
1279        if (0 == $this->_chunkLength) {
1280            $line = $this->_sock->readLine();
1281            if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
1282                $this->_chunkLength = hexdec($matches[1]);
1283                // Chunk with zero length indicates the end
1284                if (0 == $this->_chunkLength) {
1285                    $this->_sock->readLine(); // make this an eof()
1286                    return '';
1287                }
1288            } else {
1289                return '';
1290            }
1291        }
1292        $data = $this->_sock->read($this->_chunkLength);
1293        $this->_chunkLength -= strlen($data);
1294        if (0 == $this->_chunkLength) {
1295            $this->_sock->readLine(); // Trailing CRLF
1296        }
1297        return $data;
1298    }
1299
1300
1301   /**
1302    * Notifies all registered listeners of an event.
1303    *
1304    * @param    string  Event name
1305    * @param    mixed   Additional data
1306    * @access   private
1307    * @see HTTP_Request::_notify()
1308    */
1309    function _notify($event, $data = null)
1310    {
1311        foreach (array_keys($this->_listeners) as $id) {
1312            $this->_listeners[$id]->update($this, $event, $data);
1313        }
1314    }
1315
1316
1317   /**
1318    * Decodes the message-body encoded by gzip
1319    *
1320    * The real decoding work is done by gzinflate() built-in function, this
1321    * method only parses the header and checks data for compliance with
1322    * RFC 1952 
1323    *
1324    * @access   private
1325    * @param    string  gzip-encoded data
1326    * @return   string  decoded data
1327    */
1328    function _decodeGzip($data)
1329    {
1330        $length = strlen($data);
1331        // If it doesn't look like gzip-encoded data, don't bother
1332        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
1333            return $data;
1334        }
1335        $method = ord(substr($data, 2, 1));
1336        if (8 != $method) {
1337            return PEAR::raiseError('_decodeGzip(): unknown compression method');
1338        }
1339        $flags = ord(substr($data, 3, 1));
1340        if ($flags & 224) {
1341            return PEAR::raiseError('_decodeGzip(): reserved bits are set');
1342        }
1343
1344        // header is 10 bytes minimum. may be longer, though.
1345        $headerLength = 10;
1346        // extra fields, need to skip 'em
1347        if ($flags & 4) {
1348            if ($length - $headerLength - 2 < 8) {
1349                return PEAR::raiseError('_decodeGzip(): data too short');
1350            }
1351            $extraLength = unpack('v', substr($data, 10, 2));
1352            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
1353                return PEAR::raiseError('_decodeGzip(): data too short');
1354            }
1355            $headerLength += $extraLength[1] + 2;
1356        }
1357        // file name, need to skip that
1358        if ($flags & 8) {
1359            if ($length - $headerLength - 1 < 8) {
1360                return PEAR::raiseError('_decodeGzip(): data too short');
1361            }
1362            $filenameLength = strpos(substr($data, $headerLength), chr(0));
1363            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
1364                return PEAR::raiseError('_decodeGzip(): data too short');
1365            }
1366            $headerLength += $filenameLength + 1;
1367        }
1368        // comment, need to skip that also
1369        if ($flags & 16) {
1370            if ($length - $headerLength - 1 < 8) {
1371                return PEAR::raiseError('_decodeGzip(): data too short');
1372            }
1373            $commentLength = strpos(substr($data, $headerLength), chr(0));
1374            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
1375                return PEAR::raiseError('_decodeGzip(): data too short');
1376            }
1377            $headerLength += $commentLength + 1;
1378        }
1379        // have a CRC for header. let's check
1380        if ($flags & 1) {
1381            if ($length - $headerLength - 2 < 8) {
1382                return PEAR::raiseError('_decodeGzip(): data too short');
1383            }
1384            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
1385            $crcStored = unpack('v', substr($data, $headerLength, 2));
1386            if ($crcReal != $crcStored[1]) {
1387                return PEAR::raiseError('_decodeGzip(): header CRC check failed');
1388            }
1389            $headerLength += 2;
1390        }
1391        // unpacked data CRC and size at the end of encoded data
1392        $tmp = unpack('V2', substr($data, -8));
1393        $dataCrc  = $tmp[1];
1394        $dataSize = $tmp[2];
1395
1396        // finally, call the gzinflate() function
1397        $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize);
1398        if (false === $unpacked) {
1399            return PEAR::raiseError('_decodeGzip(): gzinflate() call failed');
1400        } elseif ($dataSize != strlen($unpacked)) {
1401            return PEAR::raiseError('_decodeGzip(): data size check failed');
1402        } elseif ($dataCrc != crc32($unpacked)) {
1403            return PEAR::raiseError('_decodeGzip(): data CRC check failed');
1404        }
1405        return $unpacked;
1406    }
1407} // End class HTTP_Response
1408?>
Note: See TracBrowser for help on using the repository browser.