home *** CD-ROM | disk | FTP | other *** search
/ Enter 2004 June / ENTER.ISO / files / xampp-win32-1.4.5-installer.exe / xampp / Request.php < prev    next >
Encoding:
PHP Script  |  2004-03-24  |  32.6 KB  |  1,059 lines

  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,v 1.33 2003/10/26 10:28:28 avb Exp $
  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.  
  46. require_once('Net/Socket.php');
  47. require_once('Net/URL.php');
  48.  
  49. define('HTTP_REQUEST_METHOD_GET',     'GET',     true);
  50. define('HTTP_REQUEST_METHOD_HEAD',    'HEAD',    true);
  51. define('HTTP_REQUEST_METHOD_POST',    'POST',    true);
  52. define('HTTP_REQUEST_METHOD_PUT',     'PUT',     true);
  53. define('HTTP_REQUEST_METHOD_DELETE',  'DELETE',  true);
  54. define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
  55. define('HTTP_REQUEST_METHOD_TRACE',   'TRACE',   true);
  56.  
  57. define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
  58. define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);
  59.  
  60. class HTTP_Request {
  61.  
  62.     /**
  63.     * Full url
  64.     * @var string
  65.     */
  66.     var $_url;
  67.  
  68.     /**
  69.     * Type of request
  70.     * @var string
  71.     */
  72.     var $_method;
  73.  
  74.     /**
  75.     * HTTP Version
  76.     * @var string
  77.     */
  78.     var $_http;
  79.  
  80.     /**
  81.     * Request headers
  82.     * @var array
  83.     */
  84.     var $_requestHeaders;
  85.  
  86.     /**
  87.     * Basic Auth Username
  88.     * @var string
  89.     */
  90.     var $_user;
  91.     
  92.     /**
  93.     * Basic Auth Password
  94.     * @var string
  95.     */
  96.     var $_pass;
  97.  
  98.     /**
  99.     * Socket object
  100.     * @var object
  101.     */
  102.     var $_sock;
  103.     
  104.     /**
  105.     * Proxy server
  106.     * @var string
  107.     */
  108.     var $_proxy_host;
  109.     
  110.     /**
  111.     * Proxy port
  112.     * @var integer
  113.     */
  114.     var $_proxy_port;
  115.     
  116.     /**
  117.     * Proxy username
  118.     * @var string
  119.     */
  120.     var $_proxy_user;
  121.     
  122.     /**
  123.     * Proxy password
  124.     * @var string
  125.     */
  126.     var $_proxy_pass;
  127.  
  128.     /**
  129.     * Post data
  130.     * @var mixed
  131.     */
  132.     var $_postData;
  133.  
  134.    /**
  135.     * Files to post 
  136.     * @var array
  137.     */
  138.     var $_postFiles = array();
  139.  
  140.     /**
  141.     * Connection timeout.
  142.     * @var integer
  143.     */
  144.     var $_timeout;
  145.     
  146.     /**
  147.     * HTTP_Response object
  148.     * @var object
  149.     */
  150.     var $_response;
  151.     
  152.     /**
  153.     * Whether to allow redirects
  154.     * @var boolean
  155.     */
  156.     var $_allowRedirects;
  157.     
  158.     /**
  159.     * Maximum redirects allowed
  160.     * @var integer
  161.     */
  162.     var $_maxRedirects;
  163.     
  164.     /**
  165.     * Current number of redirects
  166.     * @var integer
  167.     */
  168.     var $_redirects;
  169.  
  170.    /**
  171.     * Whether to append brackets [] to array variables
  172.     * @var bool
  173.     */
  174.     var $_useBrackets = true;
  175.  
  176.    /**
  177.     * Attached listeners
  178.     * @var array
  179.     */
  180.     var $_listeners = array();
  181.  
  182.    /**
  183.     * Whether to save response body in response object property  
  184.     * @var bool
  185.     */
  186.     var $_saveBody = true;
  187.  
  188.     /**
  189.     * Constructor
  190.     *
  191.     * Sets up the object
  192.     * @param $url The url to fetch/access
  193.     * @param $params Associative array of parameters which can be:
  194.     *                  method         - Method to use, GET, POST etc
  195.     *                  http           - HTTP Version to use, 1.0 or 1.1
  196.     *                  user           - Basic Auth username
  197.     *                  pass           - Basic Auth password
  198.     *                  proxy_host     - Proxy server host
  199.     *                  proxy_port     - Proxy server port
  200.     *                  proxy_user     - Proxy auth username
  201.     *                  proxy_pass     - Proxy auth password
  202.     *                  timeout        - Connection timeout in seconds.
  203.     *                  allowRedirects - Whether to follow redirects or not
  204.     *                  maxRedirects   - Max number of redirects to follow
  205.     *                  useBrackets    - Whether to append [] to array variable names
  206.     *                  saveBody       - Whether to save response body in response object property
  207.     * @access public
  208.     */
  209.     function HTTP_Request($url, $params = array())
  210.     {
  211.         $this->_sock           = &new Net_Socket();
  212.         $this->_method         =  HTTP_REQUEST_METHOD_GET;
  213.         $this->_http           =  HTTP_REQUEST_HTTP_VER_1_1;
  214.         $this->_requestHeaders = array();
  215.         $this->_postData       = null;
  216.  
  217.         $this->_user = null;
  218.         $this->_pass = null;
  219.  
  220.         $this->_proxy_host = null;
  221.         $this->_proxy_port = null;
  222.         $this->_proxy_user = null;
  223.         $this->_proxy_pass = null;
  224.  
  225.         $this->_allowRedirects = false;
  226.         $this->_maxRedirects   = 3;
  227.         $this->_redirects      = 0;
  228.  
  229.         $this->_timeout  = null;
  230.         $this->_response = null;
  231.  
  232.         foreach ($params as $key => $value) {
  233.             $this->{'_' . $key} = $value;
  234.         }
  235.  
  236.         $this->setURL($url);
  237.  
  238.         // Default useragent
  239.         $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');
  240.  
  241.         // Default Content-Type
  242.         $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
  243.  
  244.         // Make sure keepalives dont knobble us
  245.         $this->addHeader('Connection', 'close');
  246.  
  247.         // Basic authentication
  248.         if (!empty($this->_user)) {
  249.             $this->_requestHeaders['Authorization'] = 'Basic ' . base64_encode($this->_user . ':' . $this->_pass);
  250.         }
  251.  
  252.         // Use gzip encoding if possible
  253.         if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) {
  254.             $this->addHeader('Accept-Encoding', 'gzip');
  255.         }
  256.     }
  257.     
  258.     /**
  259.     * Generates a Host header for HTTP/1.1 requests
  260.     *
  261.     * @access private
  262.     * @return string
  263.     */
  264.     function _generateHostHeader()
  265.     {
  266.         if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {
  267.             $host = $this->_url->host . ':' . $this->_url->port;
  268.  
  269.         } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {
  270.             $host = $this->_url->host . ':' . $this->_url->port;
  271.  
  272.         } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {
  273.             $host = $this->_url->host . ':' . $this->_url->port;
  274.         
  275.         } else {
  276.             $host = $this->_url->host;
  277.         }
  278.  
  279.         return $host;
  280.     }
  281.     
  282.     /**
  283.     * Resets the object to its initial state (DEPRECATED).
  284.     * Takes the same parameters as the constructor.
  285.     *
  286.     * @param  string $url    The url to be requested
  287.     * @param  array  $params Associative array of parameters
  288.     *                        (see constructor for details)
  289.     * @access public
  290.     * @deprecated deprecated since 1.2, call the constructor if this is necessary
  291.     */
  292.     function reset($url, $params = array())
  293.     {
  294.         $this->HTTP_Request($url, $params);
  295.     }
  296.  
  297.     /**
  298.     * Sets the URL to be requested
  299.     *
  300.     * @param  string The url to be requested
  301.     * @access public
  302.     */
  303.     function setURL($url)
  304.     {
  305.         $this->_url = &new Net_URL($url, $this->_useBrackets);
  306.  
  307.         // If port is 80 and protocol is https, assume port 443 is to be used
  308.         // This does mean you can't send an https request to port 80 without
  309.         // some fudge. (mmm...)
  310.         if (strcasecmp($this->_url->protocol, 'https') == 0 AND $this->_url->port == 80) {
  311.             $this->_url->port = 443;
  312.         }
  313.         if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
  314.             $this->addHeader('Host', $this->_generateHostHeader());
  315.         }
  316.     }
  317.     
  318.     /**
  319.     * Sets a proxy to be used
  320.     *
  321.     * @param string     Proxy host
  322.     * @param int        Proxy port
  323.     * @param string     Proxy username
  324.     * @param string     Proxy password
  325.     * @access public
  326.     */
  327.     function setProxy($host, $port = 8080, $user = null, $pass = null)
  328.     {
  329.         $this->_proxy_host = $host;
  330.         $this->_proxy_port = $port;
  331.         $this->_proxy_user = $user;
  332.         $this->_proxy_pass = $pass;
  333.  
  334.         if (!empty($user)) {
  335.             $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
  336.         }
  337.     }
  338.  
  339.     /**
  340.     * Sets basic authentication parameters
  341.     *
  342.     * @param string     Username
  343.     * @param string     Password
  344.     */
  345.     function setBasicAuth($user, $pass)
  346.     {
  347.         $this->_user = $user;
  348.         $this->_pass = $pass;
  349.  
  350.         $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
  351.     }
  352.  
  353.     /**
  354.     * Sets the method to be used, GET, POST etc.
  355.     *
  356.     * @param string     Method to use. Use the defined constants for this
  357.     * @access public
  358.     */
  359.     function setMethod($method)
  360.     {
  361.         $this->_method = $method;
  362.     }
  363.  
  364.     /**
  365.     * Sets the HTTP version to use, 1.0 or 1.1
  366.     *
  367.     * @param string     Version to use. Use the defined constants for this
  368.     * @access public
  369.     */
  370.     function setHttpVer($http)
  371.     {
  372.         $this->_http = $http;
  373.     }
  374.  
  375.     /**
  376.     * Adds a request header
  377.     *
  378.     * @param string     Header name
  379.     * @param string     Header value
  380.     * @access public
  381.     */
  382.     function addHeader($name, $value)
  383.     {
  384.         $this->_requestHeaders[$name] = $value;
  385.     }
  386.  
  387.     /**
  388.     * Removes a request header
  389.     *
  390.     * @param string     Header name to remove
  391.     * @access public
  392.     */
  393.     function removeHeader($name)
  394.     {
  395.         if (isset($this->_requestHeaders[$name])) {
  396.             unset($this->_requestHeaders[$name]);
  397.         }
  398.     }
  399.  
  400.     /**
  401.     * Adds a querystring parameter
  402.     *
  403.     * @param string     Querystring parameter name
  404.     * @param string     Querystring parameter value
  405.     * @param bool       Whether the value is already urlencoded or not, default = not
  406.     * @access public
  407.     */
  408.     function addQueryString($name, $value, $preencoded = false)
  409.     {
  410.         $this->_url->addQueryString($name, $value, $preencoded);
  411.     }    
  412.     
  413.     /**
  414.     * Sets the querystring to literally what you supply
  415.     *
  416.     * @param string     The querystring data. Should be of the format foo=bar&x=y etc
  417.     * @param bool       Whether data is already urlencoded or not, default = already encoded
  418.     * @access public
  419.     */
  420.     function addRawQueryString($querystring, $preencoded = true)
  421.     {
  422.         $this->_url->addRawQueryString($querystring, $preencoded);
  423.     }
  424.  
  425.     /**
  426.     * Adds postdata items
  427.     *
  428.     * @param string     Post data name
  429.     * @param string     Post data value
  430.     * @param bool       Whether data is already urlencoded or not, default = not
  431.     * @access public
  432.     */
  433.     function addPostData($name, $value, $preencoded = false)
  434.     {
  435.         if ($preencoded) {
  436.             $this->_postData[$name] = $value;
  437.         } else {
  438.             $this->_postData[$name] = is_array($value)? array_map('urlencode', $value): urlencode($value);
  439.         }
  440.     }
  441.  
  442.    /**
  443.     * Adds a file to upload
  444.     * 
  445.     * This also changes content-type to 'multipart/form-data' for proper upload
  446.     * 
  447.     * @access public
  448.     * @param  string    variable name
  449.     * @param  mixed     file name(s)
  450.     * @param  mixed     content-type(s) of file(s) being uploaded
  451.     * @return bool      true on success
  452.     * @throws PEAR_Error
  453.     */
  454.     function addFile($name, $filename, $contentType = 'application/octet-stream')
  455.     {
  456.         if (!is_array($filename) && !is_readable($filename)) {
  457.             return PEAR::raiseError("File '{$filename}' is not readable");
  458.         } elseif (is_array($filename)) {
  459.             foreach ($filename as $name) {
  460.                 if (!is_readable($name)) {
  461.                     return PEAR::raiseError("File '{$name}' is not readable");
  462.                 }
  463.             }
  464.         }
  465.         $this->addHeader('Content-Type', 'multipart/form-data');
  466.         $this->_postFiles[$name] = array(
  467.             'name' => $filename,
  468.             'type' => $contentType
  469.         );
  470.         return true;
  471.     }
  472.  
  473.     /**
  474.     * Adds raw postdata
  475.     *
  476.     * @param string     The data
  477.     * @param bool       Whether data is preencoded or not, default = already encoded
  478.     * @access public
  479.     */
  480.     function addRawPostData($postdata, $preencoded = true)
  481.     {
  482.         $this->_postData = $preencoded ? $postdata : urlencode($postdata);
  483.     }
  484.  
  485.     /**
  486.     * Clears any postdata that has been added (DEPRECATED). 
  487.     * 
  488.     * Useful for multiple request scenarios.
  489.     *
  490.     * @access public
  491.     * @deprecated deprecated since 1.2
  492.     */
  493.     function clearPostData()
  494.     {
  495.         $this->_postData = null;
  496.     }
  497.  
  498.     /**
  499.     * Appends a cookie to "Cookie:" header
  500.     * 
  501.     * @param string $name cookie name
  502.     * @param string $value cookie value
  503.     * @access public
  504.     */
  505.     function addCookie($name, $value)
  506.     {
  507.         $cookies = isset($this->_requestHeaders['Cookie']) ? $this->_requestHeaders['Cookie']. '; ' : '';
  508.         $this->addHeader('Cookie', $cookies . urlencode($name) . '=' . urlencode($value));
  509.     }
  510.     
  511.     /**
  512.     * Clears any cookies that have been added (DEPRECATED). 
  513.     * 
  514.     * Useful for multiple request scenarios
  515.     *
  516.     * @access public
  517.     * @deprecated deprecated since 1.2
  518.     */
  519.     function clearCookies()
  520.     {
  521.         $this->removeHeader('Cookie');
  522.     }
  523.  
  524.     /**
  525.     * Sends the request
  526.     *
  527.     * @access public
  528.     * @param  bool   Whether to store response body in Response object property,
  529.     *                set this to false if downloading a LARGE file and using a Listener
  530.     * @return mixed  PEAR error on error, true otherwise
  531.     */
  532.     function sendRequest($saveBody = true)
  533.     {
  534.         $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
  535.         $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;
  536.  
  537.         // 4.3.0 supports SSL connections using OpenSSL. The function test determines
  538.         // we running on at least 4.3.0
  539.         if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) {
  540.             if (isset($this->_proxy_host)) {
  541.                 return PEAR::raiseError('HTTPS proxies are not supported.');
  542.             }
  543.             $host = 'ssl://' . $host;
  544.         }
  545.  
  546.         // If this is a second request, we may get away without
  547.         // re-connecting if they're on the same server
  548.         if (   PEAR::isError($err = $this->_sock->connect($host, $port, null, $this->_timeout))
  549.             OR PEAR::isError($err = $this->_sock->write($this->_buildRequest())) ) {
  550.  
  551.            return $err;
  552.         }
  553.  
  554.         $this->_notify('sentRequest');
  555.  
  556.         // Read the response
  557.         $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
  558.         if (PEAR::isError($err = $this->_response->process($this->_saveBody && $saveBody)) ) {
  559.             return $err;
  560.         }
  561.  
  562.         // Check for redirection
  563.         // Bugfix (PEAR) bug #18, 6 oct 2003 by Dave Mertens (headers are also stored lowercase, so we're gonna use them here)
  564.         // some non RFC2616 compliant servers (scripts) are returning lowercase headers ('location: xxx')
  565.         if (    $this->_allowRedirects
  566.             AND $this->_redirects <= $this->_maxRedirects
  567.             AND $this->getResponseCode() > 300
  568.             AND $this->getResponseCode() < 399
  569.             AND !empty($this->_response->_headers['location'])) {
  570.  
  571.             
  572.             $redirect = $this->_response->_headers['location'];
  573.  
  574.             // Absolute URL
  575.             if (preg_match('/^https?:\/\//i', $redirect)) {
  576.                 $this->_url = &new Net_URL($redirect);
  577.                 $this->addHeader('Host', $this->_generateHostHeader());
  578.             // Absolute path
  579.             } elseif ($redirect{0} == '/') {
  580.                 $this->_url->path = $redirect;
  581.             
  582.             // Relative path
  583.             } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
  584.                 if (substr($this->_url->path, -1) == '/') {
  585.                     $redirect = $this->_url->path . $redirect;
  586.                 } else {
  587.                     $redirect = dirname($this->_url->path) . '/' . $redirect;
  588.                 }
  589.                 $redirect = Net_URL::resolvePath($redirect);
  590.                 $this->_url->path = $redirect;
  591.                 
  592.             // Filename, no path
  593.             } else {
  594.                 if (substr($this->_url->path, -1) == '/') {
  595.                     $redirect = $this->_url->path . $redirect;
  596.                 } else {
  597.                     $redirect = dirname($this->_url->path) . '/' . $redirect;
  598.                 }
  599.                 $this->_url->path = $redirect;
  600.             }
  601.  
  602.             $this->_redirects++;
  603.             return $this->sendRequest($saveBody);
  604.  
  605.         // Too many redirects
  606.         } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
  607.             return PEAR::raiseError('Too many redirects');
  608.         }
  609.         
  610.         return true;
  611.     }
  612.  
  613.     /**
  614.     * Returns the response code
  615.     *
  616.     * @access public
  617.     * @return int
  618.     */
  619.     function getResponseCode()
  620.     {
  621.         return isset($this->_response->_code) ? $this->_response->_code : false;
  622.     }
  623.  
  624.     /**
  625.     * Returns either the named header or all if no name given
  626.     *
  627.     * @access public
  628.     * @param string     The header name to return
  629.     * @return mixed     either the value of $headername or an array of all header values
  630.     */
  631.     function getResponseHeader($headername = null)
  632.     {
  633.         if (!isset($headername)) {
  634.             return $this->_response->_headers;
  635.         } else {
  636.             return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
  637.         }
  638.     }
  639.  
  640.     /**
  641.     * Returns the body of the response
  642.     *
  643.     * @access public
  644.     * @return string
  645.     */
  646.     function getResponseBody()
  647.     {
  648.         return isset($this->_response->_body) ? $this->_response->_body : false;
  649.     }
  650.  
  651.     /**
  652.     * Returns cookies set in response
  653.     * 
  654.     * @access public
  655.     * @return array
  656.     */
  657.     function getResponseCookies()
  658.     {
  659.         return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
  660.     }
  661.  
  662.     /**
  663.     * Builds the request string
  664.     *
  665.     * @access private
  666.     * @return string The request string
  667.     */
  668.     function _buildRequest()
  669.     {
  670.         $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
  671.  
  672.         $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
  673.         $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
  674.         $path = (empty($this->_url->path)? '/': $this->_url->path) . $querystring;
  675.         $url  = $host . $port . $path;
  676.  
  677.         $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
  678.  
  679.         if ('multipart/form-data' == $this->_requestHeaders['Content-Type']) {
  680.             $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
  681.             $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
  682.         }
  683.  
  684.         // Request Headers
  685.         if (!empty($this->_requestHeaders)) {
  686.             foreach ($this->_requestHeaders as $name => $value) {
  687.                 $request .= $name . ': ' . $value . "\r\n";
  688.             }
  689.         }
  690.  
  691.         // Post data if it's an array
  692.         if ((!empty($this->_postData) && is_array($this->_postData)) || !empty($this->_postFiles)) {
  693.             // multipart request, probably with file uploads
  694.             if (isset($boundary)) {
  695.                 $postdata = '';
  696.                 foreach ($this->_postData as $name => $value) {
  697.                     if (is_array($value)) {
  698.                         foreach ($value as $k => $v) {
  699.                             $postdata .= '--' . $boundary . "\r\n";
  700.                             $postdata .= 'Content-Disposition: form-data; name="' . $name . ($this->_useBrackets? '[' . $k . ']': '') . '"';
  701.                             $postdata .= "\r\n\r\n" . urldecode($v) . "\r\n";
  702.                         }
  703.                     } else {
  704.                         $postdata .= '--' . $boundary . "\r\n";
  705.                         $postdata .= 'Content-Disposition: form-data; name="' . $name . '"';
  706.                         $postdata .= "\r\n\r\n" . urldecode($value) . "\r\n";
  707.                     }
  708.                 }
  709.                 foreach ($this->_postFiles as $name => $value) {
  710.                     if (is_array($value['name'])) {
  711.                         $varname       = $name . ($this->_useBrackets? '[]': '');
  712.                     } else {
  713.                         $varname       = $name;
  714.                         $value['name'] = array($value['name']);
  715.                     }
  716.                     foreach ($value['name'] as $key => $filename) {
  717.                         $fp   = fopen($filename, 'r');
  718.                         $data = fread($fp, filesize($filename));
  719.                         fclose($fp);
  720.                         $basename = basename($filename);
  721.                         $type     = is_array($value['type'])? @$value['type'][$key]: $value['type'];
  722.  
  723.                         $postdata .= '--' . $boundary . "\r\n";
  724.                         $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
  725.                         $postdata .= "\r\nContent-Type: " . $type;
  726.                         $postdata .= "\r\n\r\n" . $data . "\r\n";
  727.                     }
  728.                 }
  729.                 $postdata .= '--' . $boundary . "\r\n";
  730.             } else {
  731.                 foreach($this->_postData as $name => $value) {
  732.                     if (is_array($value)) {
  733.                         foreach ($value as $k => $v) {
  734.                             $postdata[] = $this->_useBrackets? sprintf('%s[%s]=%s', $name, $k, $v): $name . '=' . $v;
  735.                         }
  736.                     } else {
  737.                         $postdata[] = $name . '=' . $value;
  738.                     }
  739.                 }
  740.     
  741.                 $postdata = implode('&', $postdata);
  742.             }
  743.             $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n";
  744.             $request .= $postdata;
  745.  
  746.         // Post data if it's raw
  747.         } elseif(!empty($this->_postData)) {
  748.             $request .= 'Content-Length: ' . strlen($this->_postData) . "\r\n\r\n";
  749.             $request .= $this->_postData;
  750.  
  751.         // No post data, so simply add a final CRLF
  752.         } else {
  753.             $request .= "\r\n";
  754.         }
  755.         
  756.         return $request;
  757.     }
  758.  
  759.  
  760.    /**
  761.     * Adds a Listener to the list of listeners that are notified of
  762.     * the object's events
  763.     * 
  764.     * @param    object   HTTP_Request_Listener instance to attach
  765.     * @return   boolean  whether the listener was successfully attached
  766.     * @access   public
  767.     */
  768.     function attach(&$listener)
  769.     {
  770.         if (!is_a($listener, 'HTTP_Request_Listener')) {
  771.             return false;
  772.         }
  773.         $this->_listeners[$listener->getId()] =& $listener;
  774.         return true;
  775.     }
  776.  
  777.  
  778.    /**
  779.     * Removes a Listener from the list of listeners 
  780.     * 
  781.     * @param    object   HTTP_Request_Listener instance to detach
  782.     * @return   boolean  whether the listener was successfully detached
  783.     * @access   public
  784.     */
  785.     function detach(&$listener)
  786.     {
  787.         if (!is_a($listener, 'HTTP_Request_Listener') || 
  788.             !isset($this->_listeners[$listener->getId()])) {
  789.             return false;
  790.         }
  791.         unset($this->_listeners[$listener->getId()]);
  792.         return true;
  793.     }
  794.  
  795.  
  796.    /**
  797.     * Notifies all registered listeners of an event.
  798.     * 
  799.     * Events sent by HTTP_Request object
  800.     * 'sentRequest': after the request was sent
  801.     * Events sent by HTTP_Response object
  802.     * 'gotHeaders': after receiving response headers (headers are passed in $data)
  803.     * 'tick': on receiving a part of response body (the part is passed in $data)
  804.     * 'gzTick': on receiving a gzip-encoded part of response body (ditto)
  805.     * 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
  806.     * 
  807.     * @param    string  Event name
  808.     * @param    mixed   Additional data
  809.     * @access   private
  810.     */
  811.     function _notify($event, $data = null)
  812.     {
  813.         foreach (array_keys($this->_listeners) as $id) {
  814.             $this->_listeners[$id]->update($this, $event, $data);
  815.         }
  816.     }
  817. }
  818.  
  819.  
  820. /**
  821. * Response class to complement the Request class
  822. */
  823. class HTTP_Response
  824. {
  825.     /**
  826.     * Socket object
  827.     * @var object
  828.     */
  829.     var $_sock;
  830.  
  831.     /**
  832.     * Protocol
  833.     * @var string
  834.     */
  835.     var $_protocol;
  836.     
  837.     /**
  838.     * Return code
  839.     * @var string
  840.     */
  841.     var $_code;
  842.     
  843.     /**
  844.     * Response headers
  845.     * @var array
  846.     */
  847.     var $_headers;
  848.  
  849.     /**
  850.     * Cookies set in response  
  851.     * @var array
  852.     */
  853.     var $_cookies;
  854.  
  855.     /**
  856.     * Response body
  857.     * @var string
  858.     */
  859.     var $_body = '';
  860.  
  861.    /**
  862.     * Used by _readChunked(): remaining length of the current chunk
  863.     * @var string
  864.     */
  865.     var $_chunkLength = 0;
  866.  
  867.    /**
  868.     * Attached listeners
  869.     * @var array
  870.     */
  871.     var $_listeners = array();
  872.  
  873.     /**
  874.     * Constructor
  875.     *
  876.     * @param  object Net_Socket     socket to read the response from
  877.     * @param  array                 listeners attached to request
  878.     * @return mixed PEAR Error on error, true otherwise
  879.     */
  880.     function HTTP_Response(&$sock, &$listeners)
  881.     {
  882.         $this->_sock      =& $sock;
  883.         $this->_listeners =& $listeners;
  884.     }
  885.  
  886.  
  887.    /**
  888.     * Processes a HTTP response
  889.     * 
  890.     * This extracts response code, headers, cookies and decodes body if it 
  891.     * was encoded in some way
  892.     *
  893.     * @access public
  894.     * @param  bool      Whether to store response body in object property, set
  895.     *                   this to false if downloading a LARGE file and using a Listener.
  896.     *                   This is assumed to be true if body is gzip-encoded.
  897.     * @throws PEAR_Error
  898.     * @return mixed     true on success, PEAR_Error in case of malformed response
  899.     */
  900.     function process($saveBody = true)
  901.     {
  902.         do {
  903.             $line = $this->_sock->readLine();
  904.             if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
  905.                 return PEAR::raiseError('Malformed response.');
  906.             } else {
  907.                 $this->_protocol = 'HTTP/' . $http_version;
  908.                 $this->_code     = intval($returncode);
  909.             }
  910.             while ('' !== ($header = $this->_sock->readLine())) {
  911.                 $this->_processHeader($header);
  912.             }
  913.         } while (100 == $this->_code);
  914.  
  915.         $this->_notify('gotHeaders', $this->_headers);
  916.  
  917.         // If response body is present, read it and decode
  918.         $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
  919.         $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
  920.         $hasBody = false;
  921.         while (!$this->_sock->eof()) {
  922.             if ($chunked) {
  923.                 $data = $this->_readChunked();
  924.             } else {
  925.                 $data = $this->_sock->read(4096);
  926.             }
  927.             if ('' != $data) {
  928.                 $hasBody = true;
  929.                 if ($saveBody || $gzipped) {
  930.                     $this->_body .= $data;
  931.                 }
  932.                 $this->_notify($gzipped? 'gzTick': 'tick', $data);
  933.             }
  934.         }
  935.         if ($hasBody) {
  936.             // Uncompress the body if needed
  937.             if ($gzipped) {
  938.                 $this->_body = gzinflate(substr($this->_body, 10));
  939.                 $this->_notify('gotBody', $this->_body);
  940.             } else {
  941.                 $this->_notify('gotBody');
  942.             }
  943.         }
  944.         return true;
  945.     }
  946.  
  947.  
  948.    /**
  949.     * Processes the response header
  950.     *
  951.     * @access private
  952.     * @param  string    HTTP header
  953.     */
  954.     function _processHeader($header)
  955.     {
  956.         list($headername, $headervalue) = explode(':', $header, 2);
  957.         $headername_i = strtolower($headername);
  958.         $headervalue  = ltrim($headervalue);
  959.         
  960.         if ('set-cookie' != $headername_i) {
  961.             $this->_headers[$headername]   = $headervalue;
  962.             $this->_headers[$headername_i] = $headervalue;
  963.         } else {
  964.             $this->_parseCookie($headervalue);
  965.         }
  966.     }
  967.  
  968.  
  969.    /**
  970.     * Parse a Set-Cookie header to fill $_cookies array
  971.     *
  972.     * @access private
  973.     * @param  string    value of Set-Cookie header
  974.     */
  975.     function _parseCookie($headervalue)
  976.     {
  977.         $cookie = array(
  978.             'expires' => null,
  979.             'domain'  => null,
  980.             'path'    => null,
  981.             'secure'  => false
  982.         );
  983.  
  984.         // Only a name=value pair
  985.         if (!strpos($headervalue, ';')) {
  986.             list($cookie['name'], $cookie['value']) = array_map('trim', explode('=', $headervalue));
  987.             $cookie['name']  = urldecode($cookie['name']);
  988.             $cookie['value'] = urldecode($cookie['value']);
  989.  
  990.         // Some optional parameters are supplied
  991.         } else {
  992.             $elements = explode(';', $headervalue);
  993.             list($cookie['name'], $cookie['value']) = array_map('trim', explode('=', $elements[0]));
  994.             $cookie['name']  = urldecode($cookie['name']);
  995.             $cookie['value'] = urldecode($cookie['value']);
  996.  
  997.             for ($i = 1; $i < count($elements);$i++) {
  998.                 list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
  999.                 if ('secure' == $elName) {
  1000.                     $cookie['secure'] = true;
  1001.                 } elseif ('expires' == $elName) {
  1002.                     $cookie['expires'] = str_replace('"', '', $elValue);
  1003.                 } elseif ('path' == $elName OR 'domain' == $elName) {
  1004.                     $cookie[$elName] = urldecode($elValue);
  1005.                 } else {
  1006.                     $cookie[$elName] = $elValue;
  1007.                 }
  1008.             }
  1009.         }
  1010.         $this->_cookies[] = $cookie;
  1011.     }
  1012.  
  1013.  
  1014.    /**
  1015.     * Read a part of response body encoded with chunked Transfer-Encoding
  1016.     * 
  1017.     * @access private
  1018.     * @return string
  1019.     */
  1020.     function _readChunked()
  1021.     {
  1022.         // at start of the next chunk?
  1023.         if (0 == $this->_chunkLength) {
  1024.             $line = $this->_sock->readLine();
  1025.             if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
  1026.                 $this->_chunkLength = hexdec($matches[1]); 
  1027.                 // Chunk with zero length indicates the end
  1028.                 if (0 == $this->_chunkLength) {
  1029.                     $this->_sock->readAll(); // make this an eof()
  1030.                     return '';
  1031.                 }
  1032.             }
  1033.         }
  1034.         $data = $this->_sock->read($this->_chunkLength);
  1035.         $this->_chunkLength -= strlen($data);
  1036.         if (0 == $this->_chunkLength) {
  1037.             $this->_sock->readLine(); // Trailing CRLF
  1038.         }
  1039.         return $data;
  1040.     }
  1041.  
  1042.  
  1043.    /**
  1044.     * Notifies all registered listeners of an event.
  1045.     * 
  1046.     * @param    string  Event name
  1047.     * @param    mixed   Additional data
  1048.     * @access   private
  1049.     * @see HTTP_Request::_notify()
  1050.     */
  1051.     function _notify($event, $data = null)
  1052.     {
  1053.         foreach (array_keys($this->_listeners) as $id) {
  1054.             $this->_listeners[$id]->update($this, $event, $data);
  1055.         }
  1056.     }
  1057. } // End class HTTP_Response
  1058. ?>
  1059.