You are here

Request.php in Flickr API 5

File

phpFlickr/PEAR/HTTP/Request.php
View source
<?php

/**
 * Class for performing HTTP requests
 *
 * PHP versions 4 and 5
 * 
 * LICENSE:
 *
 * Copyright (c) 2002-2007, Richard Heyes
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * o Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * o Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * o The names of the authors may not be used to endorse or promote
 *   products derived from this software without specific prior written
 *   permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category    HTTP
 * @package     HTTP_Request
 * @author      Richard Heyes <richard@phpguru.org>
 * @author      Alexey Borzov <avb@php.net>
 * @copyright   2002-2007 Richard Heyes
 * @license     http://opensource.org/licenses/bsd-license.php New BSD License
 * @link        http://pear.php.net/package/HTTP_Request/ 
 */

/**
 * PEAR and PEAR_Error classes (for error handling)
 */
require_once 'PEAR.php';

/**
 * Socket class
 */
require_once 'Net/Socket.php';

/**
 * URL handling class
 */
require_once 'Net/URL.php';

/**#@+
 * Constants for HTTP request methods
 */
define('HTTP_REQUEST_METHOD_GET', 'GET', true);
define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true);
define('HTTP_REQUEST_METHOD_POST', 'POST', true);
define('HTTP_REQUEST_METHOD_PUT', 'PUT', true);
define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true);
define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true);

/**#@-*/

/**#@+
 * Constants for HTTP request error codes
 */
define('HTTP_REQUEST_ERROR_FILE', 1);
define('HTTP_REQUEST_ERROR_URL', 2);
define('HTTP_REQUEST_ERROR_PROXY', 4);
define('HTTP_REQUEST_ERROR_REDIRECTS', 8);
define('HTTP_REQUEST_ERROR_RESPONSE', 16);
define('HTTP_REQUEST_ERROR_GZIP_METHOD', 32);
define('HTTP_REQUEST_ERROR_GZIP_READ', 64);
define('HTTP_REQUEST_ERROR_GZIP_DATA', 128);
define('HTTP_REQUEST_ERROR_GZIP_CRC', 256);

/**#@-*/

/**#@+
 * Constants for HTTP protocol versions
 */
define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);

/**#@-*/
if (extension_loaded('mbstring') && 2 & ini_get('mbstring.func_overload')) {

  /**
   * Whether string functions are overloaded by their mbstring equivalents
   */
  define('HTTP_REQUEST_MBSTRING', true);
}
else {

  /**
   * @ignore
   */
  define('HTTP_REQUEST_MBSTRING', false);
}

/**
 * Class for performing HTTP requests
 *
 * Simple example (fetches yahoo.com and displays it):
 * <code>
 * $a = &new HTTP_Request('http://www.yahoo.com/');
 * $a->sendRequest();
 * echo $a->getResponseBody();
 * </code>
 *
 * @category    HTTP
 * @package     HTTP_Request
 * @author      Richard Heyes <richard@phpguru.org>
 * @author      Alexey Borzov <avb@php.net>
 * @version     Release: 1.4.2
 */
class HTTP_Request {

  /**#@+
   * @access private
   */

  /**
   * Instance of Net_URL
   * @var Net_URL
   */
  var $_url;

  /**
   * Type of request
   * @var string
   */
  var $_method;

  /**
   * HTTP Version
   * @var string
   */
  var $_http;

  /**
   * Request headers
   * @var array
   */
  var $_requestHeaders;

  /**
   * Basic Auth Username
   * @var string
   */
  var $_user;

  /**
   * Basic Auth Password
   * @var string
   */
  var $_pass;

  /**
   * Socket object
   * @var Net_Socket
   */
  var $_sock;

  /**
   * Proxy server
   * @var string
   */
  var $_proxy_host;

  /**
   * Proxy port
   * @var integer
   */
  var $_proxy_port;

  /**
   * Proxy username
   * @var string
   */
  var $_proxy_user;

  /**
   * Proxy password
   * @var string
   */
  var $_proxy_pass;

  /**
   * Post data
   * @var array
   */
  var $_postData;

  /**
   * Request body
   * @var string
   */
  var $_body;

  /**
   * A list of methods that MUST NOT have a request body, per RFC 2616
   * @var array
   */
  var $_bodyDisallowed = [
    'TRACE',
  ];

  /**
   * Files to post
   * @var array
   */
  var $_postFiles = [];

  /**
   * Connection timeout.
   * @var float
   */
  var $_timeout;

  /**
   * HTTP_Response object
   * @var HTTP_Response
   */
  var $_response;

  /**
   * Whether to allow redirects
   * @var boolean
   */
  var $_allowRedirects;

  /**
   * Maximum redirects allowed
   * @var integer
   */
  var $_maxRedirects;

  /**
   * Current number of redirects
   * @var integer
   */
  var $_redirects;

  /**
   * Whether to append brackets [] to array variables
   * @var bool
   */
  var $_useBrackets = true;

  /**
   * Attached listeners
   * @var array
   */
  var $_listeners = [];

  /**
   * Whether to save response body in response object property
   * @var bool
   */
  var $_saveBody = true;

  /**
   * Timeout for reading from socket (array(seconds, microseconds))
   * @var array
   */
  var $_readTimeout = null;

  /**
   * Options to pass to Net_Socket::connect. See stream_context_create
   * @var array
   */
  var $_socketOptions = null;

  /**#@-*/

  /**
   * Constructor
   *
   * Sets up the object
   * @param    string  The url to fetch/access
   * @param    array   Associative array of parameters which can have the following keys:
   * <ul>
   *   <li>method         - Method to use, GET, POST etc (string)</li>
   *   <li>http           - HTTP Version to use, 1.0 or 1.1 (string)</li>
   *   <li>user           - Basic Auth username (string)</li>
   *   <li>pass           - Basic Auth password (string)</li>
   *   <li>proxy_host     - Proxy server host (string)</li>
   *   <li>proxy_port     - Proxy server port (integer)</li>
   *   <li>proxy_user     - Proxy auth username (string)</li>
   *   <li>proxy_pass     - Proxy auth password (string)</li>
   *   <li>timeout        - Connection timeout in seconds (float)</li>
   *   <li>allowRedirects - Whether to follow redirects or not (bool)</li>
   *   <li>maxRedirects   - Max number of redirects to follow (integer)</li>
   *   <li>useBrackets    - Whether to append [] to array variable names (bool)</li>
   *   <li>saveBody       - Whether to save response body in response object property (bool)</li>
   *   <li>readTimeout    - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>
   *   <li>socketOptions  - Options to pass to Net_Socket object (array)</li>
   * </ul>
   * @access public
   */
  function HTTP_Request($url = '', $params = []) {
    $this->_method = HTTP_REQUEST_METHOD_GET;
    $this->_http = HTTP_REQUEST_HTTP_VER_1_1;
    $this->_requestHeaders = array();
    $this->_postData = array();
    $this->_body = null;
    $this->_user = null;
    $this->_pass = null;
    $this->_proxy_host = null;
    $this->_proxy_port = null;
    $this->_proxy_user = null;
    $this->_proxy_pass = null;
    $this->_allowRedirects = false;
    $this->_maxRedirects = 3;
    $this->_redirects = 0;
    $this->_timeout = null;
    $this->_response = null;
    foreach ($params as $key => $value) {
      $this->{'_' . $key} = $value;
    }
    if (!empty($url)) {
      $this
        ->setURL($url);
    }

    // Default useragent
    $this
      ->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');

    // We don't do keep-alives by default
    $this
      ->addHeader('Connection', 'close');

    // Basic authentication
    if (!empty($this->_user)) {
      $this
        ->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));
    }

    // Proxy authentication (see bug #5913)
    if (!empty($this->_proxy_user)) {
      $this
        ->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass));
    }

    // Use gzip encoding if possible
    if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) {
      $this
        ->addHeader('Accept-Encoding', 'gzip');
    }
  }

  /**
   * Generates a Host header for HTTP/1.1 requests
   *
   * @access private
   * @return string
   */
  function _generateHostHeader() {
    if ($this->_url->port != 80 and strcasecmp($this->_url->protocol, 'http') == 0) {
      $host = $this->_url->host . ':' . $this->_url->port;
    }
    elseif ($this->_url->port != 443 and strcasecmp($this->_url->protocol, 'https') == 0) {
      $host = $this->_url->host . ':' . $this->_url->port;
    }
    elseif ($this->_url->port == 443 and strcasecmp($this->_url->protocol, 'https') == 0 and strpos($this->_url->url, ':443') !== false) {
      $host = $this->_url->host . ':' . $this->_url->port;
    }
    else {
      $host = $this->_url->host;
    }
    return $host;
  }

  /**
   * Resets the object to its initial state (DEPRECATED).
   * Takes the same parameters as the constructor.
   *
   * @param  string $url    The url to be requested
   * @param  array  $params Associative array of parameters
   *                        (see constructor for details)
   * @access public
   * @deprecated deprecated since 1.2, call the constructor if this is necessary
   */
  function reset($url, $params = []) {
    $this
      ->HTTP_Request($url, $params);
  }

  /**
   * Sets the URL to be requested
   *
   * @param  string The url to be requested
   * @access public
   */
  function setURL($url) {
    $this->_url =& new Net_URL($url, $this->_useBrackets);
    if (!empty($this->_url->user) || !empty($this->_url->pass)) {
      $this
        ->setBasicAuth($this->_url->user, $this->_url->pass);
    }
    if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
      $this
        ->addHeader('Host', $this
        ->_generateHostHeader());
    }

    // set '/' instead of empty path rather than check later (see bug #8662)
    if (empty($this->_url->path)) {
      $this->_url->path = '/';
    }
  }

  /**
   * Returns the current request URL
   *
   * @return   string  Current request URL
   * @access   public
   */
  function getUrl() {
    return empty($this->_url) ? '' : $this->_url
      ->getUrl();
  }

  /**
   * Sets a proxy to be used
   *
   * @param string     Proxy host
   * @param int        Proxy port
   * @param string     Proxy username
   * @param string     Proxy password
   * @access public
   */
  function setProxy($host, $port = 8080, $user = null, $pass = null) {
    $this->_proxy_host = $host;
    $this->_proxy_port = $port;
    $this->_proxy_user = $user;
    $this->_proxy_pass = $pass;
    if (!empty($user)) {
      $this
        ->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
    }
  }

  /**
   * Sets basic authentication parameters
   *
   * @param string     Username
   * @param string     Password
   */
  function setBasicAuth($user, $pass) {
    $this->_user = $user;
    $this->_pass = $pass;
    $this
      ->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
  }

  /**
   * Sets the method to be used, GET, POST etc.
   *
   * @param string     Method to use. Use the defined constants for this
   * @access public
   */
  function setMethod($method) {
    $this->_method = $method;
  }

  /**
   * Sets the HTTP version to use, 1.0 or 1.1
   *
   * @param string     Version to use. Use the defined constants for this
   * @access public
   */
  function setHttpVer($http) {
    $this->_http = $http;
  }

  /**
   * Adds a request header
   *
   * @param string     Header name
   * @param string     Header value
   * @access public
   */
  function addHeader($name, $value) {
    $this->_requestHeaders[strtolower($name)] = $value;
  }

  /**
   * Removes a request header
   *
   * @param string     Header name to remove
   * @access public
   */
  function removeHeader($name) {
    if (isset($this->_requestHeaders[strtolower($name)])) {
      unset($this->_requestHeaders[strtolower($name)]);
    }
  }

  /**
   * Adds a querystring parameter
   *
   * @param string     Querystring parameter name
   * @param string     Querystring parameter value
   * @param bool       Whether the value is already urlencoded or not, default = not
   * @access public
   */
  function addQueryString($name, $value, $preencoded = false) {
    $this->_url
      ->addQueryString($name, $value, $preencoded);
  }

  /**
   * Sets the querystring to literally what you supply
   *
   * @param string     The querystring data. Should be of the format foo=bar&x=y etc
   * @param bool       Whether data is already urlencoded or not, default = already encoded
   * @access public
   */
  function addRawQueryString($querystring, $preencoded = true) {
    $this->_url
      ->addRawQueryString($querystring, $preencoded);
  }

  /**
   * Adds postdata items
   *
   * @param string     Post data name
   * @param string     Post data value
   * @param bool       Whether data is already urlencoded or not, default = not
   * @access public
   */
  function addPostData($name, $value, $preencoded = false) {
    if ($preencoded) {
      $this->_postData[$name] = $value;
    }
    else {
      $this->_postData[$name] = $this
        ->_arrayMapRecursive('urlencode', $value);
    }
  }

  /**
   * Recursively applies the callback function to the value
   *
   * @param    mixed   Callback function
   * @param    mixed   Value to process
   * @access   private
   * @return   mixed   Processed value
   */
  function _arrayMapRecursive($callback, $value) {
    if (!is_array($value)) {
      return call_user_func($callback, $value);
    }
    else {
      $map = array();
      foreach ($value as $k => $v) {
        $map[$k] = $this
          ->_arrayMapRecursive($callback, $v);
      }
      return $map;
    }
  }

  /**
   * Adds a file to upload
   *
   * This also changes content-type to 'multipart/form-data' for proper upload
   *
   * @access public
   * @param  string    name of file-upload field
   * @param  mixed     file name(s)
   * @param  mixed     content-type(s) of file(s) being uploaded
   * @return bool      true on success
   * @throws PEAR_Error
   */
  function addFile($inputName, $fileName, $contentType = 'application/octet-stream') {
    if (!is_array($fileName) && !is_readable($fileName)) {
      return PEAR::raiseError("File '{$fileName}' is not readable", HTTP_REQUEST_ERROR_FILE);
    }
    elseif (is_array($fileName)) {
      foreach ($fileName as $name) {
        if (!is_readable($name)) {
          return PEAR::raiseError("File '{$name}' is not readable", HTTP_REQUEST_ERROR_FILE);
        }
      }
    }
    $this
      ->addHeader('Content-Type', 'multipart/form-data');
    $this->_postFiles[$inputName] = array(
      'name' => $fileName,
      'type' => $contentType,
    );
    return true;
  }

  /**
   * Adds raw postdata (DEPRECATED)
   *
   * @param string     The data
   * @param bool       Whether data is preencoded or not, default = already encoded
   * @access public
   * @deprecated       deprecated since 1.3.0, method setBody() should be used instead
   */
  function addRawPostData($postdata, $preencoded = true) {
    $this->_body = $preencoded ? $postdata : urlencode($postdata);
  }

  /**
   * Sets the request body (for POST, PUT and similar requests)
   *
   * @param    string  Request body
   * @access   public
   */
  function setBody($body) {
    $this->_body = $body;
  }

  /**
   * Clears any postdata that has been added (DEPRECATED).
   *
   * Useful for multiple request scenarios.
   *
   * @access public
   * @deprecated deprecated since 1.2
   */
  function clearPostData() {
    $this->_postData = null;
  }

  /**
   * Appends a cookie to "Cookie:" header
   *
   * @param string $name cookie name
   * @param string $value cookie value
   * @access public
   */
  function addCookie($name, $value) {
    $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie'] . '; ' : '';
    $this
      ->addHeader('Cookie', $cookies . $name . '=' . $value);
  }

  /**
   * Clears any cookies that have been added (DEPRECATED).
   *
   * Useful for multiple request scenarios
   *
   * @access public
   * @deprecated deprecated since 1.2
   */
  function clearCookies() {
    $this
      ->removeHeader('Cookie');
  }

  /**
   * Sends the request
   *
   * @access public
   * @param  bool   Whether to store response body in Response object property,
   *                set this to false if downloading a LARGE file and using a Listener
   * @return mixed  PEAR error on error, true otherwise
   */
  function sendRequest($saveBody = true) {
    if (!is_a($this->_url, 'Net_URL')) {
      return PEAR::raiseError('No URL given', HTTP_REQUEST_ERROR_URL);
    }
    $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
    $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;

    // 4.3.0 supports SSL connections using OpenSSL. The function test determines
    // we running on at least 4.3.0
    if (strcasecmp($this->_url->protocol, 'https') == 0 and function_exists('file_get_contents') and extension_loaded('openssl')) {
      if (isset($this->_proxy_host)) {
        return PEAR::raiseError('HTTPS proxies are not supported', HTTP_REQUEST_ERROR_PROXY);
      }
      $host = 'ssl://' . $host;
    }

    // magic quotes may fuck up file uploads and chunked response processing
    $magicQuotes = ini_get('magic_quotes_runtime');
    ini_set('magic_quotes_runtime', false);

    // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
    // connection token to a proxy server...
    if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']) {
      $this
        ->removeHeader('connection');
    }
    $keepAlive = HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection']) || !empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection'];
    $sockets =& PEAR::getStaticProperty('HTTP_Request', 'sockets');
    $sockKey = $host . ':' . $port;
    unset($this->_sock);

    // There is a connected socket in the "static" property?
    if ($keepAlive && !empty($sockets[$sockKey]) && !empty($sockets[$sockKey]->fp)) {
      $this->_sock =& $sockets[$sockKey];
      $err = null;
    }
    else {
      $this
        ->_notify('connect');
      $this->_sock =& new Net_Socket();
      $err = $this->_sock
        ->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
    }
    PEAR::isError($err) or $err = $this->_sock
      ->write($this
      ->_buildRequest());
    if (!PEAR::isError($err)) {
      if (!empty($this->_readTimeout)) {
        $this->_sock
          ->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
      }
      $this
        ->_notify('sentRequest');

      // Read the response
      $this->_response =& new HTTP_Response($this->_sock, $this->_listeners);
      $err = $this->_response
        ->process($this->_saveBody && $saveBody, HTTP_REQUEST_METHOD_HEAD != $this->_method);
      if ($keepAlive) {
        $keepAlive = isset($this->_response->_headers['content-length']) || isset($this->_response->_headers['transfer-encoding']) && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked';
        if ($keepAlive) {
          if (isset($this->_response->_headers['connection'])) {
            $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';
          }
          else {
            $keepAlive = 'HTTP/' . HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;
          }
        }
      }
    }
    ini_set('magic_quotes_runtime', $magicQuotes);
    if (PEAR::isError($err)) {
      return $err;
    }
    if (!$keepAlive) {
      $this
        ->disconnect();

      // Store the connected socket in "static" property
    }
    elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {
      $sockets[$sockKey] =& $this->_sock;
    }

    // Check for redirection
    if ($this->_allowRedirects and $this->_redirects <= $this->_maxRedirects and $this
      ->getResponseCode() > 300 and $this
      ->getResponseCode() < 399 and !empty($this->_response->_headers['location'])) {
      $redirect = $this->_response->_headers['location'];

      // Absolute URL
      if (preg_match('/^https?:\\/\\//i', $redirect)) {
        $this->_url =& new Net_URL($redirect);
        $this
          ->addHeader('Host', $this
          ->_generateHostHeader());

        // Absolute path
      }
      elseif ($redirect[0] == '/') {
        $this->_url->path = $redirect;

        // Relative path
      }
      elseif (substr($redirect, 0, 3) == '../' or substr($redirect, 0, 2) == './') {
        if (substr($this->_url->path, -1) == '/') {
          $redirect = $this->_url->path . $redirect;
        }
        else {
          $redirect = dirname($this->_url->path) . '/' . $redirect;
        }
        $redirect = Net_URL::resolvePath($redirect);
        $this->_url->path = $redirect;

        // Filename, no path
      }
      else {
        if (substr($this->_url->path, -1) == '/') {
          $redirect = $this->_url->path . $redirect;
        }
        else {
          $redirect = dirname($this->_url->path) . '/' . $redirect;
        }
        $this->_url->path = $redirect;
      }
      $this->_redirects++;
      return $this
        ->sendRequest($saveBody);

      // Too many redirects
    }
    elseif ($this->_allowRedirects and $this->_redirects > $this->_maxRedirects) {
      return PEAR::raiseError('Too many redirects', HTTP_REQUEST_ERROR_REDIRECTS);
    }
    return true;
  }

  /**
   * Disconnect the socket, if connected. Only useful if using Keep-Alive.
   *
   * @access public
   */
  function disconnect() {
    if (!empty($this->_sock) && !empty($this->_sock->fp)) {
      $this
        ->_notify('disconnect');
      $this->_sock
        ->disconnect();
    }
  }

  /**
   * Returns the response code
   *
   * @access public
   * @return mixed     Response code, false if not set
   */
  function getResponseCode() {
    return isset($this->_response->_code) ? $this->_response->_code : false;
  }

  /**
   * Returns either the named header or all if no name given
   *
   * @access public
   * @param string     The header name to return, do not set to get all headers
   * @return mixed     either the value of $headername (false if header is not present)
   *                   or an array of all headers
   */
  function getResponseHeader($headername = null) {
    if (!isset($headername)) {
      return isset($this->_response->_headers) ? $this->_response->_headers : array();
    }
    else {
      $headername = strtolower($headername);
      return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
    }
  }

  /**
   * Returns the body of the response
   *
   * @access public
   * @return mixed     response body, false if not set
   */
  function getResponseBody() {
    return isset($this->_response->_body) ? $this->_response->_body : false;
  }

  /**
   * Returns cookies set in response
   *
   * @access public
   * @return mixed     array of response cookies, false if none are present
   */
  function getResponseCookies() {
    return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
  }

  /**
   * Builds the request string
   *
   * @access private
   * @return string The request string
   */
  function _buildRequest() {
    $separator = ini_get('arg_separator.output');
    ini_set('arg_separator.output', '&');
    $querystring = ($querystring = $this->_url
      ->getQueryString()) ? '?' . $querystring : '';
    ini_set('arg_separator.output', $separator);
    $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
    $port = (isset($this->_proxy_host) and $this->_url->port != 80) ? ':' . $this->_url->port : '';
    $path = $this->_url->path . $querystring;
    $url = $host . $port . $path;
    if (!strlen($url)) {
      $url = '/';
    }
    $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
    if (in_array($this->_method, $this->_bodyDisallowed) || 0 == strlen($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method || empty($this->_postData) && empty($this->_postFiles))) {
      $this
        ->removeHeader('Content-Type');
    }
    else {
      if (empty($this->_requestHeaders['content-type'])) {

        // Add default content-type
        $this
          ->addHeader('Content-Type', 'application/x-www-form-urlencoded');
      }
      elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
        $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
        $this
          ->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
      }
    }

    // Request Headers
    if (!empty($this->_requestHeaders)) {
      foreach ($this->_requestHeaders as $name => $value) {
        $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
        $request .= $canonicalName . ': ' . $value . "\r\n";
      }
    }

    // No post data or wrong method, so simply add a final CRLF
    if (in_array($this->_method, $this->_bodyDisallowed) || HTTP_REQUEST_METHOD_POST != $this->_method && 0 == strlen($this->_body)) {
      $request .= "\r\n";

      // Post data if it's an array
    }
    elseif (HTTP_REQUEST_METHOD_POST == $this->_method && (!empty($this->_postData) || !empty($this->_postFiles))) {

      // "normal" POST request
      if (!isset($boundary)) {
        $postdata = implode('&', array_map(create_function('$a', 'return $a[0] . \'=\' . $a[1];'), $this
          ->_flattenArray('', $this->_postData)));

        // multipart request, probably with file uploads
      }
      else {
        $postdata = '';
        if (!empty($this->_postData)) {
          $flatData = $this
            ->_flattenArray('', $this->_postData);
          foreach ($flatData as $item) {
            $postdata .= '--' . $boundary . "\r\n";
            $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
            $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
          }
        }
        foreach ($this->_postFiles as $name => $value) {
          if (is_array($value['name'])) {
            $varname = $name . ($this->_useBrackets ? '[]' : '');
          }
          else {
            $varname = $name;
            $value['name'] = array(
              $value['name'],
            );
          }
          foreach ($value['name'] as $key => $filename) {
            $fp = fopen($filename, 'r');
            $data = fread($fp, filesize($filename));
            fclose($fp);
            $basename = basename($filename);
            $type = is_array($value['type']) ? @$value['type'][$key] : $value['type'];
            $postdata .= '--' . $boundary . "\r\n";
            $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
            $postdata .= "\r\nContent-Type: " . $type;
            $postdata .= "\r\n\r\n" . $data . "\r\n";
          }
        }
        $postdata .= '--' . $boundary . "--\r\n";
      }
      $request .= 'Content-Length: ' . (HTTP_REQUEST_MBSTRING ? mb_strlen($postdata, 'iso-8859-1') : strlen($postdata)) . "\r\n\r\n";
      $request .= $postdata;

      // Explicitly set request body
    }
    elseif (0 < strlen($this->_body)) {
      $request .= 'Content-Length: ' . (HTTP_REQUEST_MBSTRING ? mb_strlen($this->_body, 'iso-8859-1') : strlen($this->_body)) . "\r\n\r\n";
      $request .= $this->_body;

      // Terminate headers with CRLF on POST request with no body, too
    }
    else {
      $request .= "\r\n";
    }
    return $request;
  }

  /**
   * Helper function to change the (probably multidimensional) associative array
   * into the simple one.
   *
   * @param    string  name for item
   * @param    mixed   item's values
   * @return   array   array with the following items: array('item name', 'item value');
   * @access   private
   */
  function _flattenArray($name, $values) {
    if (!is_array($values)) {
      return array(
        array(
          $name,
          $values,
        ),
      );
    }
    else {
      $ret = array();
      foreach ($values as $k => $v) {
        if (empty($name)) {
          $newName = $k;
        }
        elseif ($this->_useBrackets) {
          $newName = $name . '[' . $k . ']';
        }
        else {
          $newName = $name;
        }
        $ret = array_merge($ret, $this
          ->_flattenArray($newName, $v));
      }
      return $ret;
    }
  }

  /**
   * Adds a Listener to the list of listeners that are notified of
   * the object's events
   *
   * Events sent by HTTP_Request object
   * - 'connect': on connection to server
   * - 'sentRequest': after the request was sent
   * - 'disconnect': on disconnection from server
   *
   * Events sent by HTTP_Response object
   * - 'gotHeaders': after receiving response headers (headers are passed in $data)
   * - 'tick': on receiving a part of response body (the part is passed in $data)
   * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)
   * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
   *
   * @param    HTTP_Request_Listener   listener to attach
   * @return   boolean                 whether the listener was successfully attached
   * @access   public
   */
  function attach(&$listener) {
    if (!is_a($listener, 'HTTP_Request_Listener')) {
      return false;
    }
    $this->_listeners[$listener
      ->getId()] =& $listener;
    return true;
  }

  /**
   * Removes a Listener from the list of listeners
   *
   * @param    HTTP_Request_Listener   listener to detach
   * @return   boolean                 whether the listener was successfully detached
   * @access   public
   */
  function detach(&$listener) {
    if (!is_a($listener, 'HTTP_Request_Listener') || !isset($this->_listeners[$listener
      ->getId()])) {
      return false;
    }
    unset($this->_listeners[$listener
      ->getId()]);
    return true;
  }

  /**
   * Notifies all registered listeners of an event.
   *
   * @param    string  Event name
   * @param    mixed   Additional data
   * @access   private
   * @see      HTTP_Request::attach()
   */
  function _notify($event, $data = null) {
    foreach (array_keys($this->_listeners) as $id) {
      $this->_listeners[$id]
        ->update($this, $event, $data);
    }
  }

}

/**
 * Response class to complement the Request class
 *
 * @category    HTTP
 * @package     HTTP_Request
 * @author      Richard Heyes <richard@phpguru.org>
 * @author      Alexey Borzov <avb@php.net>
 * @version     Release: 1.4.2
 */
class HTTP_Response {

  /**
   * Socket object
   * @var Net_Socket
   */
  var $_sock;

  /**
   * Protocol
   * @var string
   */
  var $_protocol;

  /**
   * Return code
   * @var string
   */
  var $_code;

  /**
   * Response headers
   * @var array
   */
  var $_headers;

  /**
   * Cookies set in response
   * @var array
   */
  var $_cookies;

  /**
   * Response body
   * @var string
   */
  var $_body = '';

  /**
   * Used by _readChunked(): remaining length of the current chunk
   * @var string
   */
  var $_chunkLength = 0;

  /**
   * Attached listeners
   * @var array
   */
  var $_listeners = [];

  /**
   * Bytes left to read from message-body
   * @var null|int
   */
  var $_toRead;

  /**
   * Constructor
   *
   * @param  Net_Socket    socket to read the response from
   * @param  array         listeners attached to request
   */
  function HTTP_Response(&$sock, &$listeners) {
    $this->_sock =& $sock;
    $this->_listeners =& $listeners;
  }

  /**
   * Processes a HTTP response
   *
   * This extracts response code, headers, cookies and decodes body if it
   * was encoded in some way
   *
   * @access public
   * @param  bool      Whether to store response body in object property, set
   *                   this to false if downloading a LARGE file and using a Listener.
   *                   This is assumed to be true if body is gzip-encoded.
   * @param  bool      Whether the response can actually have a message-body.
   *                   Will be set to false for HEAD requests.
   * @throws PEAR_Error
   * @return mixed     true on success, PEAR_Error in case of malformed response
   */
  function process($saveBody = true, $canHaveBody = true) {
    do {
      $line = $this->_sock
        ->readLine();
      if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
        return PEAR::raiseError('Malformed response', HTTP_REQUEST_ERROR_RESPONSE);
      }
      else {
        $this->_protocol = 'HTTP/' . $http_version;
        $this->_code = intval($returncode);
      }
      while ('' !== ($header = $this->_sock
        ->readLine())) {
        $this
          ->_processHeader($header);
      }
    } while (100 == $this->_code);
    $this
      ->_notify('gotHeaders', $this->_headers);

    // RFC 2616, section 4.4:
    // 1. Any response message which "MUST NOT" include a message-body ...
    // is always terminated by the first empty line after the header fields
    // 3. ... If a message is received with both a
    // Transfer-Encoding header field and a Content-Length header field,
    // the latter MUST be ignored.
    $canHaveBody = $canHaveBody && $this->_code >= 200 && $this->_code != 204 && $this->_code != 304;

    // If response body is present, read it and decode
    $chunked = isset($this->_headers['transfer-encoding']) && 'chunked' == $this->_headers['transfer-encoding'];
    $gzipped = isset($this->_headers['content-encoding']) && 'gzip' == $this->_headers['content-encoding'];
    $hasBody = false;
    if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || 0 != $this->_headers['content-length'])) {
      if ($chunked || !isset($this->_headers['content-length'])) {
        $this->_toRead = null;
      }
      else {
        $this->_toRead = $this->_headers['content-length'];
      }
      while (!$this->_sock
        ->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {
        if ($chunked) {
          $data = $this
            ->_readChunked();
        }
        elseif (is_null($this->_toRead)) {
          $data = $this->_sock
            ->read(4096);
        }
        else {
          $data = $this->_sock
            ->read(min(4096, $this->_toRead));
          $this->_toRead -= HTTP_REQUEST_MBSTRING ? mb_strlen($data, 'iso-8859-1') : strlen($data);
        }
        if ('' == $data) {
          break;
        }
        else {
          $hasBody = true;
          if ($saveBody || $gzipped) {
            $this->_body .= $data;
          }
          $this
            ->_notify($gzipped ? 'gzTick' : 'tick', $data);
        }
      }
    }
    if ($hasBody) {

      // Uncompress the body if needed
      if ($gzipped) {
        $body = $this
          ->_decodeGzip($this->_body);
        if (PEAR::isError($body)) {
          return $body;
        }
        $this->_body = $body;
        $this
          ->_notify('gotBody', $this->_body);
      }
      else {
        $this
          ->_notify('gotBody');
      }
    }
    return true;
  }

  /**
   * Processes the response header
   *
   * @access private
   * @param  string    HTTP header
   */
  function _processHeader($header) {
    if (false === strpos($header, ':')) {
      return;
    }
    list($headername, $headervalue) = explode(':', $header, 2);
    $headername = strtolower($headername);
    $headervalue = ltrim($headervalue);
    if ('set-cookie' != $headername) {
      if (isset($this->_headers[$headername])) {
        $this->_headers[$headername] .= ',' . $headervalue;
      }
      else {
        $this->_headers[$headername] = $headervalue;
      }
    }
    else {
      $this
        ->_parseCookie($headervalue);
    }
  }

  /**
   * Parse a Set-Cookie header to fill $_cookies array
   *
   * @access private
   * @param  string    value of Set-Cookie header
   */
  function _parseCookie($headervalue) {
    $cookie = array(
      'expires' => null,
      'domain' => null,
      'path' => null,
      'secure' => false,
    );

    // Only a name=value pair
    if (!strpos($headervalue, ';')) {
      $pos = strpos($headervalue, '=');
      $cookie['name'] = trim(substr($headervalue, 0, $pos));
      $cookie['value'] = trim(substr($headervalue, $pos + 1));

      // Some optional parameters are supplied
    }
    else {
      $elements = explode(';', $headervalue);
      $pos = strpos($elements[0], '=');
      $cookie['name'] = trim(substr($elements[0], 0, $pos));
      $cookie['value'] = trim(substr($elements[0], $pos + 1));
      for ($i = 1; $i < count($elements); $i++) {
        if (false === strpos($elements[$i], '=')) {
          $elName = trim($elements[$i]);
          $elValue = null;
        }
        else {
          list($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
        }
        $elName = strtolower($elName);
        if ('secure' == $elName) {
          $cookie['secure'] = true;
        }
        elseif ('expires' == $elName) {
          $cookie['expires'] = str_replace('"', '', $elValue);
        }
        elseif ('path' == $elName || 'domain' == $elName) {
          $cookie[$elName] = urldecode($elValue);
        }
        else {
          $cookie[$elName] = $elValue;
        }
      }
    }
    $this->_cookies[] = $cookie;
  }

  /**
   * Read a part of response body encoded with chunked Transfer-Encoding
   *
   * @access private
   * @return string
   */
  function _readChunked() {

    // at start of the next chunk?
    if (0 == $this->_chunkLength) {
      $line = $this->_sock
        ->readLine();
      if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
        $this->_chunkLength = hexdec($matches[1]);

        // Chunk with zero length indicates the end
        if (0 == $this->_chunkLength) {
          $this->_sock
            ->readLine();

          // make this an eof()
          return '';
        }
      }
      else {
        return '';
      }
    }
    $data = $this->_sock
      ->read($this->_chunkLength);
    $this->_chunkLength -= HTTP_REQUEST_MBSTRING ? mb_strlen($data, 'iso-8859-1') : strlen($data);
    if (0 == $this->_chunkLength) {
      $this->_sock
        ->readLine();

      // Trailing CRLF
    }
    return $data;
  }

  /**
   * Notifies all registered listeners of an event.
   *
   * @param    string  Event name
   * @param    mixed   Additional data
   * @access   private
   * @see HTTP_Request::_notify()
   */
  function _notify($event, $data = null) {
    foreach (array_keys($this->_listeners) as $id) {
      $this->_listeners[$id]
        ->update($this, $event, $data);
    }
  }

  /**
   * Decodes the message-body encoded by gzip
   *
   * The real decoding work is done by gzinflate() built-in function, this
   * method only parses the header and checks data for compliance with
   * RFC 1952
   *
   * @access   private
   * @param    string  gzip-encoded data
   * @return   string  decoded data
   */
  function _decodeGzip($data) {
    if (HTTP_REQUEST_MBSTRING) {
      $oldEncoding = mb_internal_encoding();
      mb_internal_encoding('iso-8859-1');
    }
    $length = strlen($data);

    // If it doesn't look like gzip-encoded data, don't bother
    if (18 > $length || strcmp(substr($data, 0, 2), "")) {
      return $data;
    }
    $method = ord(substr($data, 2, 1));
    if (8 != $method) {
      return PEAR::raiseError('_decodeGzip(): unknown compression method', HTTP_REQUEST_ERROR_GZIP_METHOD);
    }
    $flags = ord(substr($data, 3, 1));
    if ($flags & 224) {
      return PEAR::raiseError('_decodeGzip(): reserved bits are set', HTTP_REQUEST_ERROR_GZIP_DATA);
    }

    // header is 10 bytes minimum. may be longer, though.
    $headerLength = 10;

    // extra fields, need to skip 'em
    if ($flags & 4) {
      if ($length - $headerLength - 2 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $extraLength = unpack('v', substr($data, 10, 2));
      if ($length - $headerLength - 2 - $extraLength[1] < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $headerLength += $extraLength[1] + 2;
    }

    // file name, need to skip that
    if ($flags & 8) {
      if ($length - $headerLength - 1 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $filenameLength = strpos(substr($data, $headerLength), chr(0));
      if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $headerLength += $filenameLength + 1;
    }

    // comment, need to skip that also
    if ($flags & 16) {
      if ($length - $headerLength - 1 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $commentLength = strpos(substr($data, $headerLength), chr(0));
      if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $headerLength += $commentLength + 1;
    }

    // have a CRC for header. let's check
    if ($flags & 1) {
      if ($length - $headerLength - 2 < 8) {
        return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
      }
      $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));
      $crcStored = unpack('v', substr($data, $headerLength, 2));
      if ($crcReal != $crcStored[1]) {
        return PEAR::raiseError('_decodeGzip(): header CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
      }
      $headerLength += 2;
    }

    // unpacked data CRC and size at the end of encoded data
    $tmp = unpack('V2', substr($data, -8));
    $dataCrc = $tmp[1];
    $dataSize = $tmp[2];

    // finally, call the gzinflate() function
    $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize);
    if (false === $unpacked) {
      return PEAR::raiseError('_decodeGzip(): gzinflate() call failed', HTTP_REQUEST_ERROR_GZIP_READ);
    }
    elseif ($dataSize != strlen($unpacked)) {
      return PEAR::raiseError('_decodeGzip(): data size check failed', HTTP_REQUEST_ERROR_GZIP_READ);
    }
    elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
      return PEAR::raiseError('_decodeGzip(): data CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
    }
    if (HTTP_REQUEST_MBSTRING) {
      mb_internal_encoding($oldEncoding);
    }
    return $unpacked;
  }

}

// End class HTTP_Response