You are here

class HTTP_Response in Flickr API 5

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

Hierarchy

Expanded class hierarchy of HTTP_Response

File

phpFlickr/PEAR/HTTP/Request.php, line 1100

View source
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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
HTTP_Response::$_body property Response body
HTTP_Response::$_chunkLength property Used by _readChunked(): remaining length of the current chunk
HTTP_Response::$_code property Return code
HTTP_Response::$_cookies property Cookies set in response
HTTP_Response::$_headers property Response headers
HTTP_Response::$_listeners property Attached listeners
HTTP_Response::$_protocol property Protocol
HTTP_Response::$_sock property Socket object
HTTP_Response::$_toRead property Bytes left to read from message-body
HTTP_Response::HTTP_Response function Constructor
HTTP_Response::process function Processes a HTTP response
HTTP_Response::_decodeGzip function Decodes the message-body encoded by gzip
HTTP_Response::_notify function Notifies all registered listeners of an event.
HTTP_Response::_parseCookie function Parse a Set-Cookie header to fill $_cookies array
HTTP_Response::_processHeader function Processes the response header
HTTP_Response::_readChunked function Read a part of response body encoded with chunked Transfer-Encoding