From 967669a49532860ec50931781b58d0f28975c63c Mon Sep 17 00:00:00 2001 From: lsces Date: Sun, 5 Apr 2026 11:10:49 +0100 Subject: PEAR changes to support PHP8.4 .. Ideally need a generic package that is more modern? --- includes/pear/HTTP/Download.php | 6 +- includes/pear/HTTP/Header.php | 17 +- includes/pear/HTTP/Request.php | 1514 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1523 insertions(+), 14 deletions(-) mode change 100644 => 100755 includes/pear/HTTP/Download.php mode change 100644 => 100755 includes/pear/HTTP/Header.php create mode 100755 includes/pear/HTTP/Request.php (limited to 'includes/pear/HTTP') diff --git a/includes/pear/HTTP/Download.php b/includes/pear/HTTP/Download.php old mode 100644 new mode 100755 index 8cfc348..de94a8f --- a/includes/pear/HTTP/Download.php +++ b/includes/pear/HTTP/Download.php @@ -247,9 +247,9 @@ class HTTP_Download * * @see HTTP_Download::setContentDisposition() */ - function HTTP_Download($params = array()) + function HTTP_Download($params = []) { - $this->HTTP = &new HTTP_Header; + $this->HTTP = new HTTP_Header; $this->_error = $this->setParams($params); } // }}} @@ -763,7 +763,7 @@ class HTTP_Download */ function staticSend($params, $guess = false) { - $d = &new HTTP_Download(); + $d = new HTTP_Download(); $e = $d->setParams($params); if (PEAR::isError($e)) { return $e; diff --git a/includes/pear/HTTP/Header.php b/includes/pear/HTTP/Header.php old mode 100644 new mode 100755 index 14026ad..926d79d --- a/includes/pear/HTTP/Header.php +++ b/includes/pear/HTTP/Header.php @@ -382,11 +382,7 @@ class HTTP_Header extends HTTP if (is_array($param) && count($param)) { if (count($param)) { foreach ($param as $key => $val) { - if (is_string($key)) { - $qs[] = urlencode($key) .'='. urlencode($val); - } else { - $qs[] = urlencode($val) .'='. urlencode(@$GLOBALS[$val]); - } + $qs[] = ( is_string( $key ) ) ? urlencode( $key ) . '=' . urlencode( $val ) : urlencode( $val ) . '=' . urlencode( @$GLOBALS[$val] ); } } } @@ -457,7 +453,7 @@ class HTTP_Header extends HTTP function isInformational($http_code) { if ($status_type = $this->getStatusType($http_code)) { - return $status_type{0} == HTTP_HEADER_STATUS_INFORMATIONAL; + return $status_type[0] == HTTP_HEADER_STATUS_INFORMATIONAL; } else { return false; } @@ -471,7 +467,7 @@ class HTTP_Header extends HTTP function isSuccessful($http_code) { if ($status_type = $this->getStatusType($http_code)) { - return $status_type{0} == HTTP_HEADER_STATUS_SUCCESSFUL; + return $status_type[0] == HTTP_HEADER_STATUS_SUCCESSFUL; } else { return false; } @@ -485,7 +481,7 @@ class HTTP_Header extends HTTP function isRedirect($http_code) { if ($status_type = $this->getStatusType($http_code)) { - return $status_type{0} == HTTP_HEADER_STATUS_REDIRECT; + return $status_type[0] == HTTP_HEADER_STATUS_REDIRECT; } else { return false; } @@ -499,7 +495,7 @@ class HTTP_Header extends HTTP function isClientError($http_code) { if ($status_type = $this->getStatusType($http_code)) { - return $status_type{0} == HTTP_HEADER_STATUS_CLIENT_ERROR; + return $status_type[0] == HTTP_HEADER_STATUS_CLIENT_ERROR; } else { return false; } @@ -513,7 +509,7 @@ class HTTP_Header extends HTTP function isServerError($http_code) { if ($status_type = $this->getStatusType($http_code)) { - return $status_type{0} == HTTP_HEADER_STATUS_SERVER_ERROR; + return $status_type[0] == HTTP_HEADER_STATUS_SERVER_ERROR; } else { return false; } @@ -534,4 +530,3 @@ class HTTP_Header extends HTTP } /**#@-*/ } -?> diff --git a/includes/pear/HTTP/Request.php b/includes/pear/HTTP/Request.php new file mode 100755 index 0000000..43fdbfd --- /dev/null +++ b/includes/pear/HTTP/Request.php @@ -0,0 +1,1514 @@ + + * @author Alexey Borzov + * @copyright 2002-2007 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Request.php,v 1.63 2008/10/11 11:07:10 avb Exp $ + * @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): + * + * $a = &new HTTP_Request('http://www.yahoo.com/'); + * $a->sendRequest(); + * echo $a->getResponseBody(); + * + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @version Release: 1.4.4 + */ +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 = array('TRACE'); + + /** + * Methods having defined semantics for request body + * + * Content-Length header (indicating that the body follows, section 4.3 of + * RFC 2616) will be sent for these methods even if no body was added + * + * @var array + */ + var $_bodyRequired = array('POST', 'PUT'); + + /** + * Files to post + * @var array + */ + var $_postFiles = array(); + + /** + * 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 = array(); + + /** + * 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: + *
    + *
  • method - Method to use, GET, POST etc (string)
  • + *
  • http - HTTP Version to use, 1.0 or 1.1 (string)
  • + *
  • user - Basic Auth username (string)
  • + *
  • pass - Basic Auth password (string)
  • + *
  • proxy_host - Proxy server host (string)
  • + *
  • proxy_port - Proxy server port (integer)
  • + *
  • proxy_user - Proxy auth username (string)
  • + *
  • proxy_pass - Proxy auth password (string)
  • + *
  • timeout - Connection timeout in seconds (float)
  • + *
  • allowRedirects - Whether to follow redirects or not (bool)
  • + *
  • maxRedirects - Max number of redirects to follow (integer)
  • + *
  • useBrackets - Whether to append [] to array variable names (bool)
  • + *
  • saveBody - Whether to save response body in response object property (bool)
  • + *
  • readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))
  • + *
  • socketOptions - Options to pass to Net_Socket object (array)
  • + *
+ * @access public + */ + function HTTP_Request($url = '', $params = array()) + { + $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 = array()) + { + $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) + { + $this->_postData[$name] = ( $preencoded ) ? $value : $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 form-based file upload + * + * Used to emulate file upload via a HTML form. The method also sets + * Content-Type of HTTP request to 'multipart/form-data'. + * + * If you just want to send the contents of a file as the body of HTTP + * request you should use setBody() method. + * + * @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; + + if (strcasecmp( $this->_url->protocol, 'https' ) == 0) { + // Bug #14127, don't try connecting to HTTPS sites without OpenSSL + if (version_compare( PHP_VERSION, '4.3.0', '<' ) || !extension_loaded( 'openssl' )) { + return PEAR::raiseError( 'Need PHP 4.3.0 or later with OpenSSL support for https:// requests', + HTTP_REQUEST_ERROR_URL ); + } + elseif (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) { + $keepAlive = ( isset( $this->_response->_headers['connection'] ) ) ? strtolower( $this->_response->_headers['connection'] ) == 'keep-alive' : '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 ) == './') { + $redirect = ( substr( $this->_url->path, -1 ) == '/' ) ? $this->_url->path . $redirect : dirname( $this->_url->path ) . '/' . $redirect; + $redirect = Net_URL::resolvePath( $redirect ); + $this->_url->path = $redirect; + + // Filename, no path + } + else { + $redirect = ( substr( $this->_url->path, -1 ) == '/' ) ? $this->_url->path . $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 the response reason phrase + * + * @access public + * @return mixed Response reason phrase, false if not set + */ + function getResponseReason() + { + return isset( $this->_response->_reason ) ? $this->_response->_reason : 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"; + } + } + + // Method does not allow a body, simply add a final CRLF + if (in_array( $this->_method, $this->_bodyDisallowed )) { + + $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' ); + $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" . fread( $fp, filesize( $filename ) ) . "\r\n"; + fclose( $fp ); + } + } + $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; + + // No body: send a Content-Length header nonetheless (request #12900), + // but do that only for methods that require a body (bug #14740) + } + else { + + if (in_array( $this->_method, $this->_bodyRequired )) { + $request .= "Content-Length: 0\r\n"; + } + $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 + * @author Alexey Borzov + * @version Release: 1.4.4 + */ +class HTTP_Response +{ + /** + * Socket object + * @var Net_Socket + */ + var $_sock; + + /** + * Protocol + * @var string + */ + var $_protocol; + + /** + * Return code + * @var string + */ + var $_code; + + /** + * Response reason phrase + * @var string + */ + var $_reason; + + /** + * 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 = array(); + + /** + * 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 (!preg_match( '!^(HTTP/\d\.\d) (\d[3])(?: (.+))?!', $line, $s )) { + return PEAR::raiseError( 'Malformed response', HTTP_REQUEST_ERROR_RESPONSE ); + } + else { + $this->_protocol = $s[1]; + $this->_code = intval( $s[2] ); + $this->_reason = empty( $s[3] ) ? null : $s[3]; + } + 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'] )) { + $this->_toRead = ( $chunked || !isset( $this->_headers['content-length'] ) ) ? null : $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 && (!$this->_chunkLength || $this->_sock->eof())) { + 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), "\x1f\x8b")) { + 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 + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 + $unpacked = gzinflate(substr($data, $headerLength, -8)); + 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 -- cgit v1.3