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

Revision 6914, 44.9 KB checked in by kakinaka, 20 years ago (diff)

* empty log message *

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