--- a/includes/http.php Sun Mar 28 21:49:26 2010 -0400
+++ b/includes/http.php Sun Mar 28 23:10:46 2010 -0400
@@ -88,844 +88,844 @@
class Request_HTTP
{
-
- /**
- * Switch to enable or disable debugging. You want this off on production sites.
- * @var bool
- */
-
- var $debug = false;
-
- /**
- * The host the request will be sent to.
- * @var string
- */
-
- var $host = '';
-
- /**
- * The TCP port our connection is (will be) on.
- * @var int
- */
-
- var $port = 80;
-
- /**
- * The request method. Can be GET or POST, defaults to GET.
- * @var string
- */
-
- var $method = 'GET';
-
- /**
- * The URI to the remote script.
- * @var string
- */
-
- var $uri = '';
-
- /**
- * The parameters to be sent on GET.
- * @var array (associative)
- */
-
- var $parms_get = array();
-
- /**
- * The parameters to be sent on POST. Ignored if $this->method == GET.
- * @var array (associative)
- */
-
- var $parms_post = array();
-
- /**
- * The list of cookies that will be sent.
- * @var array (associative)
- */
-
- var $cookies_out = array();
-
- /**
- * Additional request headers.
- * @var array (associative)
- */
-
- var $headers = array();
-
- /**
- * Follow server-side redirects; defaults to true.
- * @var bool
- */
-
- var $follow_redirects = true;
-
- /**
- * Cached response.
- * @var string, or bool:false if the request hasn't been sent yet
- */
-
- var $response = false;
-
- /**
- * Cached response code
- * @var int set to -1 if request hasn't been sent yet
- */
-
- var $response_code = -1;
-
- /**
- * Cached response code string
- * @var string or bool:false if the request hasn't been sent yet
- */
-
- var $response_string = false;
-
- /**
- * Resource for the socket. False if a connection currently isn't going.
- * @var resource
- */
-
- var $socket = false;
-
- /**
- * True if SSL is on, defaults to false
- * @var bool
- */
-
- var $ssl = false;
-
- /**
- * The state of our request. 0 means it hasn't been made yet. 1 means the socket is open, 2 means the socket is open and the request has been written, 3 means the headers have been fetched, and 4 means the request is completed.
- * @var int
- */
-
- var $state = 0;
-
- /**
- * Constructor.
- * @param string Hostname to send to
- * @param string URI (/index.php)
- * @param string Request method - GET or POST.
- * @param int Optional. The port to open the request on. Defaults to 80.
- * @param bool If true, uses SSL (and defaults the port to 443)
- */
-
- function Request_HTTP($host, $uri, $method = 'GET', $port = 'default', $ssl = false)
- {
- if ( !preg_match('/^(?:(([a-z0-9-]+\.)*?)([a-z0-9-]+)|\[[a-f0-9:]+\])$/', $host) )
- throw new Exception(__CLASS__ . ': Invalid hostname');
- if ( $ssl )
- {
- $this->ssl = true;
- $port = ( $port === 'default' ) ? 443 : $port;
- }
- else
- {
- $this->ssl = false;
- $port = ( $port === 'default' ) ? 80 : $port;
- }
- // Yes - this really does support IPv6 URLs!
- $this->host = $host;
- $this->uri = $uri;
- if ( is_int($port) && $port >= 1 && $port <= 65535 )
- $this->port = $port;
- else
- throw new Exception(__CLASS__ . ': Invalid port');
- $method = strtoupper($method);
- if ( $method == 'GET' || $method == 'POST' )
- $this->method = $method;
- else
- throw new Exception(__CLASS__ . ': Invalid request method');
-
- $newline = "\r\n";
- $php_ver = PHP_VERSION;
- $server = ( isset($_SERVER['SERVER_SOFTWARE']) ) ? "Server: {$_SERVER['SERVER_SOFTWARE']}" : "CLI";
- $this->add_header('User-Agent', "PHP/$php_ver ({$server}; automated bot request)");
- }
-
- /**
- * Sets one or more cookies to be sent to the server.
- * @param string or array If a string, the cookie name. If an array, associative array in the form of cookiename => cookievalue
- * @param string or bool If a string, the cookie value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
- */
-
- function add_cookie($cookiename, $cookievalue = false)
- {
- if ( is_array($cookiename) && !$cookievalue )
- {
- foreach ( $cookiename as $name => $value )
- {
- $this->cookies_out[$name] = $value;
- }
- }
- else if ( is_string($cookiename) && is_string($cookievalue) )
- {
- $this->cookies_out[$cookiename] = $cookievalue;
- }
- else
- {
- throw new Exception(__METHOD__ . ': Invalid argument(s)');
- }
- }
-
- /**
- * Sets one or more request header values.
- * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
- * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
- */
-
- function add_header($headername, $headervalue = false)
- {
- if ( is_array($headername) && !$headervalue )
- {
- foreach ( $headername as $name => $value )
- {
- $this->headers[$name] = $value;
- }
- }
- else if ( is_string($headername) && is_string($headervalue) )
- {
- $this->headers[$headername] = $headervalue;
- }
- else
- {
- throw new Exception(__METHOD__ . ': Invalid argument(s)');
- }
- }
-
- /**
- * Adds one or more values to be passed on GET.
- * @param string or array If a string, the parameter name. If an array, associative array in the form of parametername => parametervalue
- * @param string or bool If a string, the parameter value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
- */
-
- function add_get($getname, $getvalue = false)
- {
- if ( is_array($getname) && !$getvalue )
- {
- foreach ( $getname as $name => $value )
- {
- $this->parms_get[$name] = $value;
- }
- }
- else if ( is_string($getname) && is_string($getvalue) )
- {
- $this->parms_get[$getname] = $getvalue;
- }
- else
- {
- throw new Exception(__METHOD__ . ': Invalid argument(s)');
- }
- }
-
- /**
- * Adds one or more values to be passed on POST.
- * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
- * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
- */
-
- function add_post($postname, $postvalue = false)
- {
- if ( is_array($postname) && !$postvalue )
- {
- foreach ( $postname as $name => $value )
- {
- $this->parms_post[$name] = $value;
- }
- }
- else if ( is_string($postname) && is_string($postvalue) )
- {
- $this->parms_post[$postname] = $postvalue;
- }
- else
- {
- throw new Exception(__METHOD__ . ': Invalid argument(s)');
- }
- }
-
- /**
- * Internal function to open up the socket.
- * @access private
- */
-
- function _sock_open(&$connection)
- {
- // Open connection
- $ssl_prepend = ( $this->ssl ) ? 'ssl://' : '';
- $connection = fsockopen($ssl_prepend . $this->host, $this->port, $errno, $errstr);
- if ( !$connection )
- throw new Exception(__METHOD__ . ": Could not make connection"); // to {$this->host}:{$this->port}: error $errno: $errstr");
-
- // 1 = socket open
- $this->state = 1;
- }
-
- /**
- * Internal function to actually write the request into the socket.
- * @access private
- */
-
- function _write_request(&$connection, &$headers, &$cookies, &$get, &$post)
- {
- $newline = "\r\n";
-
- if ( $this->debug )
- echo '<p>Connection opened. Writing main request to socket. Raw socket data follows.</p><pre>';
-
- if ( $this->debug )
- {
- echo '<hr /><div style="white-space: nowrap;">';
- echo '<p><b>' . __CLASS__ . ': Sending request</b></p><p>Request parameters:</p>';
- echo "<p><b>Headers:</b></p><pre>$headers</pre>";
- echo "<p><b>Cookies:</b> $cookies</p>";
- echo "<p><b>GET URI:</b> " . htmlspecialchars($this->uri . $get) . "</p>";
- echo "<p><b>POST DATA:</b> " . htmlspecialchars($post) . "</p>";
- echo "<pre>";
- }
-
- $portline = ( $this->port == 80 ) ? '' : ":$this->port";
-
- $this->_fputs($connection, "{$this->method} {$this->uri}{$get} HTTP/1.1{$newline}");
- $this->_fputs($connection, "Host: {$this->host}$portline{$newline}");
- $this->_fputs($connection, $headers);
- $this->_fputs($connection, $cookies);
-
- if ( $this->method == 'POST' )
- {
- // POST-specific parameters
- $post_length = strlen($post);
- $this->_fputs($connection, "Content-type: application/x-www-form-urlencoded{$newline}");
- $this->_fputs($connection, "Content-length: {$post_length}{$newline}");
- }
-
- $this->_fputs($connection, "Connection: close{$newline}");
- $this->_fputs($connection, "{$newline}");
-
- if ( $this->method == 'POST' )
- {
- $this->_fputs($connection, $post);
- }
-
- if ( $this->debug )
- echo '</pre><p>Request written. Fetching response.</p>';
-
- // 2 = request written
- $this->state = 2;
- }
-
- /**
- * Wrap up and close the socket. Nothing more than a call to fsockclose() except in debug mode.
- * @access private
- */
-
- function sock_close(&$connection)
- {
- if ( $this->debug )
- {
- echo '<p>Response fetched. Closing connection. Response text follows.</p><pre>';
- echo htmlspecialchars($this->response);
- echo '</pre></div><hr />';
- }
-
- fclose($connection);
- $this->state = 0;
- }
-
- /**
- * Internal function to grab the response code and status string
- * @access string
- */
-
- function _parse_response_code($buffer)
- {
- // Retrieve response code and status
- $pos_newline = strpos($buffer, "\n");
- $pos_carriage_return = strpos($buffer, "\r");
- $pos_end_first_line = ( $pos_carriage_return < $pos_newline && $pos_carriage_return > 0 ) ? $pos_carriage_return : $pos_newline;
-
- // First line is in format of:
- // HTTP/1.1 ### Blah blah blah(\r?)\n
- $response_code = substr($buffer, 9, 3);
- $response_string = substr($buffer, 13, ( $pos_end_first_line - 13 ) );
- $this->response_code = intval($response_code);
- $this->response_string = $response_string;
- }
-
- /**
- * Internal function to send the request.
- * @access private
- */
-
- function _send_request()
- {
- $this->concat_headers($headers, $cookies, $get, $post);
-
- if ( $this->state < 1 )
- {
- $this->_sock_open($this->socket);
- }
- if ( $this->state < 2 )
- {
- $this->_write_request($this->socket, $headers, $cookies, $get, $post);
- }
- if ( $this->state == 2 )
- {
- $buffer = $this->_read_until_newlines($this->socket);
- $this->state = 3;
- $this->_parse_response_code($buffer);
- $this->response = $buffer;
- }
- // obey redirects
- $i = 0;
- while ( $i < 20 && $this->follow_redirects )
- {
- $incoming_headers = $this->get_response_headers_array();
- if ( !$incoming_headers )
- break;
- if ( isset($incoming_headers['Location']) )
- {
- // we've been redirected...
- $new_uri = $this->_resolve_uri($incoming_headers['Location']);
- if ( !$new_uri )
- {
- // ... bad URI, ignore Location header.
- break;
- }
- // change location
- $this->host = $new_uri['host'];
- $this->port = $new_uri['port'];
- $this->uri = $new_uri['uri'];
- $get = '';
-
- // reset
- $this->sock_close($this->socket);
- $this->_sock_open($this->socket);
- $this->_write_request($this->socket, $headers, $cookies, $get, $post);
- $buffer = $this->_read_until_newlines($this->socket);
- $this->state = 3;
- $this->_parse_response_code($buffer);
- $this->response = $buffer;
- $i++;
- }
- else
- {
- break;
- }
- }
- if ( $i == 20 )
- {
- throw new Exception(__METHOD__ . ": Redirect trap. Request_HTTP doesn't do cookies, btw.");
- }
-
- if ( $this->state == 3 )
- {
- // Determine transfer encoding
- $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
- if ( preg_match("/^Content-Length: ([0-9]+)[\s]*$/mi", $this->response, $match) && !$is_chunked )
- {
- $size = intval($match[1]);
- if ( $this->debug )
- {
- echo "Pulling response using fread(), size $size\n";
- }
- $this->response .= fread($this->socket, $size);
- }
- else
- {
- if ( $this->debug )
- echo "Pulling response using chunked handler\n";
-
- $buffer = '';
- while ( !feof($this->socket) )
- {
- $part = fgets($this->socket, 1024);
- if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
- {
- $chunklen = hexdec($match[1]);
- $part = ( $chunklen > 0 ) ? fread($this->socket, $chunklen) : '';
- // remove the last newline from $part
- $part = preg_replace("/\r?\n\$/", "", $part);
- }
- $buffer .= $part;
- }
- $this->response .= $buffer;
- }
- }
- $this->state = 4;
-
- $this->sock_close($this->socket);
- $this->socket = false;
- }
-
- /**
- * Internal function to send the request but only fetch the headers. Leaves a connection open for a finish-up function.
- * @access private
- */
-
- function _send_request_headers_only()
- {
- $this->concat_headers($headers, $cookies, $get, $post);
-
- if ( $this->state < 1 )
- {
- $this->_sock_open($this->socket);
- }
- if ( $this->state < 2 )
- {
- $this->_write_request($this->socket, $headers, $cookies, $get, $post);
- }
- if ( $this->state == 2 )
- {
- $buffer = $this->_read_until_newlines($this->socket);
- $this->state = 3;
- $this->_parse_response_code($buffer);
- $this->response = $buffer;
- }
- }
-
- /**
- * Internal function to read from a socket until two consecutive newlines are hit.
- * @access private
- */
-
- function _read_until_newlines($sock)
- {
- $prev_char = '';
- $prev1_char = '';
- $prev2_char = '';
- $buf = '';
- while ( !feof($sock) )
- {
- $chr = fread($sock, 1);
- $buf .= $chr;
- if ( ( $chr == "\n" && $prev_char == "\n" ) ||
- ( $chr == "\n" && $prev_char == "\r" && $prev1_char == "\n" && $prev2_char == "\r" ) )
- {
- return $buf;
- }
- $prev2_char = $prev1_char;
- $prev1_char = $prev_char;
- $prev_char = $chr;
- }
- return $buf;
- }
-
- /**
- * Returns the response text. If the request hasn't been sent, it will be sent here.
- * @return string
- */
-
- function get_response()
- {
- if ( $this->state == 4 )
- return $this->response;
- $this->_send_request();
- return $this->response;
- }
-
- /**
- * Writes the response body to a file. This is good for conserving memory when downloading large files. If the file already exists it will be overwritten.
- * @param string File to write to
- * @param int Chunk size in KB to read from the socket. Optional and should only be needed in circumstances when extreme memory conservation is needed. Defaults to 768.
- * @param int Maximum file size. Defaults to 0, which means no limit.
- * @return bool True on success, false on failure
- */
-
- function write_response_to_file($file, $chunklen = 768, $max_file_size = 0)
- {
- if ( !is_writeable( dirname($file) ) || !file_exists( dirname($file) ) )
- {
- return false;
- }
- $handle = @fopen($file, 'w');
- if ( !$handle )
- return false;
- $chunklen = intval($chunklen);
- if ( $chunklen < 1 )
- return false;
- if ( $this->state == 4 )
- {
- // we already have the response, so cheat
- $response = $this->get_response_body();
- fwrite($handle, $response);
- }
- else
- {
- // read data from the socket, write it immediately, and unset to free memory
- $headers = $this->get_response_headers();
- $transferred_bytes = 0;
- $bandwidth_exceeded = false;
- // if transfer-encoding is chunked, read using chunk sizes the server specifies
- $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
- if ( $is_chunked )
- {
- $buffer = '';
- while ( !feof($this->socket) )
- {
- $part = fgets($this->socket, ( 1024 * $chunklen ));
- // Theoretically if the encoding is really chunked then this should always match.
- if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
- {
- $chunk_length = hexdec($match[1]);
- $part = ( $chunk_length > 0 ) ? fread($this->socket, $chunk_length) : '';
- // remove the last newline from $part
- $part = preg_replace("/\r?\n\$/m", "", $part);
- }
-
- $transferred_bytes += strlen($part);
- if ( $max_file_size && $transferred_bytes > $max_file_size )
- {
- // truncate output to $max_file_size bytes
- $partlen = $max_file_size - ( $transferred_bytes - strlen($part) );
- $part = substr($part, 0, $partlen);
- $bandwidth_exceeded = true;
- }
- fwrite($handle, $part);
- if ( $bandwidth_exceeded )
- {
- break;
- }
- }
- }
- else
- {
- $first_chunk = fread($this->socket, ( 1024 * $chunklen ));
- fwrite($handle, $first_chunk);
- while ( !feof($this->socket) )
- {
- $chunk = fread($this->socket, ( 1024 * $chunklen ));
-
- $transferred_bytes += strlen($chunk);
- if ( $max_file_size && $transferred_bytes > $max_file_size )
- {
- // truncate output to $max_file_size bytes
- $partlen = $max_file_size - ( $transferred_bytes - strlen($chunk) );
- $chunk = substr($chunk, 0, $partlen);
- $bandwidth_exceeded = true;
- }
-
- fwrite($handle, $chunk);
- unset($chunk);
-
- if ( $bandwidth_exceeded )
- {
- break;
- }
- }
- }
- }
- fclose($handle);
- // close socket and reset state, since we haven't cached the response
- $this->sock_close($this->socket);
- $this->state = 0;
- return ($bandwidth_exceeded) ? false : true;
- }
-
- /**
- * Resolves, based on current settings and URI, a URI string to an array consisting of a host, port, and new URI. Returns false on error.
- * @param string
- * @return array
- */
-
- function _resolve_uri($uri)
- {
- // long ass regexp w00t
- if ( !preg_match('#^(?:https?://((?:(?:[a-z0-9-]+\.)*)(?:[a-z0-9-]+)|\[[a-f0-9:]+\])(?::([0-9]+))?)?(/)(.*)$#i', $uri, $match) )
- {
- // bad target URI
- return false;
- }
- $hostpart = $match[1];
- if ( empty($hostpart) )
- {
- // use existing host
- $host = $this->host;
- $port = $this->port;
- }
- else
- {
- $host = $match[1];
- $port = empty($match[2]) ? 80 : intval($match[2]);
- }
- // is this an absolute URI, or relative?
- if ( empty($match[3]) )
- {
- // relative
- $uri = dirname($this->uri) . $match[4];
- }
- else
- {
- // absolute
- $uri = '/' . $match[4];
- }
- return array(
- 'host' => $host,
- 'port' => $port,
- 'uri' => $uri
- );
- }
-
- /**
- * Returns only the response headers.
- * @return string
- */
-
- function get_response_headers()
- {
- if ( $this->state == 3 )
- {
- return $this->response;
- }
- else if ( $this->state == 4 )
- {
- $pos_end = strpos($this->response, "\r\n\r\n");
- if ( empty($pos_end) )
- {
- $pos_end = strpos($this->response, "\n\n");
- }
- $data = substr($this->response, 0, $pos_end);
- return $data;
- }
- else
- {
- $this->_send_request_headers_only();
- return $this->response;
- }
- }
-
- /**
- * Returns only the response headers, as an associative array.
- * @return array
- */
-
- function get_response_headers_array()
- {
- $data = $this->get_response_headers();
- preg_match_all("/(^|\n)([A-z0-9_-]+?): (.+?)(\r|\n|\$)/", $data, $matches);
- $headers = array();
- for ( $i = 0; $i < count($matches[0]); $i++ )
- {
- $headers[ $matches[2][$i] ] = $matches[3][$i];
- }
- return $headers;
- }
-
- /**
- * Returns only the body (not the headers) of the response. If the request hasn't been sent, it will be sent here.
- * @return string
- */
-
- function get_response_body()
- {
- $data = $this->get_response();
- $pos_start = strpos($data, "\r\n\r\n") + 4;
- if ( $pos_start == 4 )
- {
- $pos_start = strpos($data, "\n\n") + 4;
- }
- $data = substr($data, $pos_start);
- return $data;
- }
-
- /**
- * Returns all cookies requested to be set by the server as an associative array. If the request hasn't been sent, it will be sent here.
- * @return array
- */
-
- function get_cookies()
- {
- $data = $this->get_response();
- $data = str_replace("\r\n", "\n", $data);
- $pos = strpos($data, "\n\n");
- $headers = substr($data, 0, $pos);
- preg_match_all("/Set-Cookie: ([a-z0-9_]+)=([^;]+);( expires=([^;]+);)?( path=(.*?))?\n/", $headers, $cookiematch);
- if ( count($cookiematch[0]) < 1 )
- return array();
- $cookies = array();
- foreach ( $cookiematch[0] as $i => $cookie )
- {
- $cookies[$cookiematch[1][$i]] = $cookiematch[2][$i];
- }
- return $cookies;
- }
-
- /**
- * Internal method to write data to a socket with debugging possibility.
- * @access private
- */
-
- function _fputs($socket, $data)
- {
- if ( $this->debug )
- echo htmlspecialchars($data);
-
- return fputs($socket, $data);
- }
-
- /**
- * Internal function to stringify cookies, headers, get, and post.
- * @access private
- */
-
- function concat_headers(&$headers, &$cookies, &$get, &$post)
- {
- $headers = '';
- $cookies = '';
- foreach ( $this->headers as $name => $value )
- {
- $value = str_replace('\\n', '\\\\n', $value);
- $value = str_replace("\n", '\\n', $value);
- $headers .= "$name: $value\r\n";
- }
- unset($value);
- if ( count($this->cookies_out) > 0 )
- {
- $i = 0;
- $cookie_header = 'Cookie: ';
- foreach ( $this->cookies_out as $name => $value )
- {
- $i++;
- if ( $i > 1 )
- $cookie_header .= '; ';
- $value = str_replace(';', rawurlencode(';'), $value);
- $value = str_replace('\\n', '\\\\n', $value);
- $value = str_replace("\n", '\\n', $value);
- $cookie_header .= "$name=$value";
- }
- $cookie_header .= "\r\n";
- $cookies = $cookie_header;
- unset($value, $cookie_header);
- }
- if ( count($this->parms_get) > 0 )
- {
- $get = '?';
- $i = 0;
- foreach ( $this->parms_get as $name => $value )
- {
- $i++;
- if ( $i > 1 )
- $get .= '&';
- $value = urlencode($value);
- if ( !empty($value) || is_string($value) )
- $get .= "$name=$value";
- else
- $get .= "$name";
- }
- }
- if ( count($this->parms_post) > 0 )
- {
- $post = '';
- $i = 0;
- foreach ( $this->parms_post as $name => $value )
- {
- $i++;
- if ( $i > 1 )
- $post .= '&';
- $value = urlencode($value);
- $post .= "$name=$value";
- }
- }
- }
-
+
+ /**
+ * Switch to enable or disable debugging. You want this off on production sites.
+ * @var bool
+ */
+
+ var $debug = false;
+
+ /**
+ * The host the request will be sent to.
+ * @var string
+ */
+
+ var $host = '';
+
+ /**
+ * The TCP port our connection is (will be) on.
+ * @var int
+ */
+
+ var $port = 80;
+
+ /**
+ * The request method. Can be GET or POST, defaults to GET.
+ * @var string
+ */
+
+ var $method = 'GET';
+
+ /**
+ * The URI to the remote script.
+ * @var string
+ */
+
+ var $uri = '';
+
+ /**
+ * The parameters to be sent on GET.
+ * @var array (associative)
+ */
+
+ var $parms_get = array();
+
+ /**
+ * The parameters to be sent on POST. Ignored if $this->method == GET.
+ * @var array (associative)
+ */
+
+ var $parms_post = array();
+
+ /**
+ * The list of cookies that will be sent.
+ * @var array (associative)
+ */
+
+ var $cookies_out = array();
+
+ /**
+ * Additional request headers.
+ * @var array (associative)
+ */
+
+ var $headers = array();
+
+ /**
+ * Follow server-side redirects; defaults to true.
+ * @var bool
+ */
+
+ var $follow_redirects = true;
+
+ /**
+ * Cached response.
+ * @var string, or bool:false if the request hasn't been sent yet
+ */
+
+ var $response = false;
+
+ /**
+ * Cached response code
+ * @var int set to -1 if request hasn't been sent yet
+ */
+
+ var $response_code = -1;
+
+ /**
+ * Cached response code string
+ * @var string or bool:false if the request hasn't been sent yet
+ */
+
+ var $response_string = false;
+
+ /**
+ * Resource for the socket. False if a connection currently isn't going.
+ * @var resource
+ */
+
+ var $socket = false;
+
+ /**
+ * True if SSL is on, defaults to false
+ * @var bool
+ */
+
+ var $ssl = false;
+
+ /**
+ * The state of our request. 0 means it hasn't been made yet. 1 means the socket is open, 2 means the socket is open and the request has been written, 3 means the headers have been fetched, and 4 means the request is completed.
+ * @var int
+ */
+
+ var $state = 0;
+
+ /**
+ * Constructor.
+ * @param string Hostname to send to
+ * @param string URI (/index.php)
+ * @param string Request method - GET or POST.
+ * @param int Optional. The port to open the request on. Defaults to 80.
+ * @param bool If true, uses SSL (and defaults the port to 443)
+ */
+
+ function Request_HTTP($host, $uri, $method = 'GET', $port = 'default', $ssl = false)
+ {
+ if ( !preg_match('/^(?:(([a-z0-9-]+\.)*?)([a-z0-9-]+)|\[[a-f0-9:]+\])$/', $host) )
+ throw new Exception(__CLASS__ . ': Invalid hostname');
+ if ( $ssl )
+ {
+ $this->ssl = true;
+ $port = ( $port === 'default' ) ? 443 : $port;
+ }
+ else
+ {
+ $this->ssl = false;
+ $port = ( $port === 'default' ) ? 80 : $port;
+ }
+ // Yes - this really does support IPv6 URLs!
+ $this->host = $host;
+ $this->uri = $uri;
+ if ( is_int($port) && $port >= 1 && $port <= 65535 )
+ $this->port = $port;
+ else
+ throw new Exception(__CLASS__ . ': Invalid port');
+ $method = strtoupper($method);
+ if ( $method == 'GET' || $method == 'POST' )
+ $this->method = $method;
+ else
+ throw new Exception(__CLASS__ . ': Invalid request method');
+
+ $newline = "\r\n";
+ $php_ver = PHP_VERSION;
+ $server = ( isset($_SERVER['SERVER_SOFTWARE']) ) ? "Server: {$_SERVER['SERVER_SOFTWARE']}" : "CLI";
+ $this->add_header('User-Agent', "PHP/$php_ver ({$server}; automated bot request)");
+ }
+
+ /**
+ * Sets one or more cookies to be sent to the server.
+ * @param string or array If a string, the cookie name. If an array, associative array in the form of cookiename => cookievalue
+ * @param string or bool If a string, the cookie value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+ */
+
+ function add_cookie($cookiename, $cookievalue = false)
+ {
+ if ( is_array($cookiename) && !$cookievalue )
+ {
+ foreach ( $cookiename as $name => $value )
+ {
+ $this->cookies_out[$name] = $value;
+ }
+ }
+ else if ( is_string($cookiename) && is_string($cookievalue) )
+ {
+ $this->cookies_out[$cookiename] = $cookievalue;
+ }
+ else
+ {
+ throw new Exception(__METHOD__ . ': Invalid argument(s)');
+ }
+ }
+
+ /**
+ * Sets one or more request header values.
+ * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
+ * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+ */
+
+ function add_header($headername, $headervalue = false)
+ {
+ if ( is_array($headername) && !$headervalue )
+ {
+ foreach ( $headername as $name => $value )
+ {
+ $this->headers[$name] = $value;
+ }
+ }
+ else if ( is_string($headername) && is_string($headervalue) )
+ {
+ $this->headers[$headername] = $headervalue;
+ }
+ else
+ {
+ throw new Exception(__METHOD__ . ': Invalid argument(s)');
+ }
+ }
+
+ /**
+ * Adds one or more values to be passed on GET.
+ * @param string or array If a string, the parameter name. If an array, associative array in the form of parametername => parametervalue
+ * @param string or bool If a string, the parameter value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+ */
+
+ function add_get($getname, $getvalue = false)
+ {
+ if ( is_array($getname) && !$getvalue )
+ {
+ foreach ( $getname as $name => $value )
+ {
+ $this->parms_get[$name] = $value;
+ }
+ }
+ else if ( is_string($getname) && is_string($getvalue) )
+ {
+ $this->parms_get[$getname] = $getvalue;
+ }
+ else
+ {
+ throw new Exception(__METHOD__ . ': Invalid argument(s)');
+ }
+ }
+
+ /**
+ * Adds one or more values to be passed on POST.
+ * @param string or array If a string, the header name. If an array, associative array in the form of headername => headervalue
+ * @param string or bool If a string, the header value. If boolean, defaults to false, param 1 should be an array, and this should not be passed.
+ */
+
+ function add_post($postname, $postvalue = false)
+ {
+ if ( is_array($postname) && !$postvalue )
+ {
+ foreach ( $postname as $name => $value )
+ {
+ $this->parms_post[$name] = $value;
+ }
+ }
+ else if ( is_string($postname) && is_string($postvalue) )
+ {
+ $this->parms_post[$postname] = $postvalue;
+ }
+ else
+ {
+ throw new Exception(__METHOD__ . ': Invalid argument(s)');
+ }
+ }
+
+ /**
+ * Internal function to open up the socket.
+ * @access private
+ */
+
+ function _sock_open(&$connection)
+ {
+ // Open connection
+ $ssl_prepend = ( $this->ssl ) ? 'ssl://' : '';
+ $connection = fsockopen($ssl_prepend . $this->host, $this->port, $errno, $errstr);
+ if ( !$connection )
+ throw new Exception(__METHOD__ . ": Could not make connection"); // to {$this->host}:{$this->port}: error $errno: $errstr");
+
+ // 1 = socket open
+ $this->state = 1;
+ }
+
+ /**
+ * Internal function to actually write the request into the socket.
+ * @access private
+ */
+
+ function _write_request(&$connection, &$headers, &$cookies, &$get, &$post)
+ {
+ $newline = "\r\n";
+
+ if ( $this->debug )
+ echo '<p>Connection opened. Writing main request to socket. Raw socket data follows.</p><pre>';
+
+ if ( $this->debug )
+ {
+ echo '<hr /><div style="white-space: nowrap;">';
+ echo '<p><b>' . __CLASS__ . ': Sending request</b></p><p>Request parameters:</p>';
+ echo "<p><b>Headers:</b></p><pre>$headers</pre>";
+ echo "<p><b>Cookies:</b> $cookies</p>";
+ echo "<p><b>GET URI:</b> " . htmlspecialchars($this->uri . $get) . "</p>";
+ echo "<p><b>POST DATA:</b> " . htmlspecialchars($post) . "</p>";
+ echo "<pre>";
+ }
+
+ $portline = ( $this->port == 80 ) ? '' : ":$this->port";
+
+ $this->_fputs($connection, "{$this->method} {$this->uri}{$get} HTTP/1.1{$newline}");
+ $this->_fputs($connection, "Host: {$this->host}$portline{$newline}");
+ $this->_fputs($connection, $headers);
+ $this->_fputs($connection, $cookies);
+
+ if ( $this->method == 'POST' )
+ {
+ // POST-specific parameters
+ $post_length = strlen($post);
+ $this->_fputs($connection, "Content-type: application/x-www-form-urlencoded{$newline}");
+ $this->_fputs($connection, "Content-length: {$post_length}{$newline}");
+ }
+
+ $this->_fputs($connection, "Connection: close{$newline}");
+ $this->_fputs($connection, "{$newline}");
+
+ if ( $this->method == 'POST' )
+ {
+ $this->_fputs($connection, $post);
+ }
+
+ if ( $this->debug )
+ echo '</pre><p>Request written. Fetching response.</p>';
+
+ // 2 = request written
+ $this->state = 2;
+ }
+
+ /**
+ * Wrap up and close the socket. Nothing more than a call to fsockclose() except in debug mode.
+ * @access private
+ */
+
+ function sock_close(&$connection)
+ {
+ if ( $this->debug )
+ {
+ echo '<p>Response fetched. Closing connection. Response text follows.</p><pre>';
+ echo htmlspecialchars($this->response);
+ echo '</pre></div><hr />';
+ }
+
+ fclose($connection);
+ $this->state = 0;
+ }
+
+ /**
+ * Internal function to grab the response code and status string
+ * @access string
+ */
+
+ function _parse_response_code($buffer)
+ {
+ // Retrieve response code and status
+ $pos_newline = strpos($buffer, "\n");
+ $pos_carriage_return = strpos($buffer, "\r");
+ $pos_end_first_line = ( $pos_carriage_return < $pos_newline && $pos_carriage_return > 0 ) ? $pos_carriage_return : $pos_newline;
+
+ // First line is in format of:
+ // HTTP/1.1 ### Blah blah blah(\r?)\n
+ $response_code = substr($buffer, 9, 3);
+ $response_string = substr($buffer, 13, ( $pos_end_first_line - 13 ) );
+ $this->response_code = intval($response_code);
+ $this->response_string = $response_string;
+ }
+
+ /**
+ * Internal function to send the request.
+ * @access private
+ */
+
+ function _send_request()
+ {
+ $this->concat_headers($headers, $cookies, $get, $post);
+
+ if ( $this->state < 1 )
+ {
+ $this->_sock_open($this->socket);
+ }
+ if ( $this->state < 2 )
+ {
+ $this->_write_request($this->socket, $headers, $cookies, $get, $post);
+ }
+ if ( $this->state == 2 )
+ {
+ $buffer = $this->_read_until_newlines($this->socket);
+ $this->state = 3;
+ $this->_parse_response_code($buffer);
+ $this->response = $buffer;
+ }
+ // obey redirects
+ $i = 0;
+ while ( $i < 20 && $this->follow_redirects )
+ {
+ $incoming_headers = $this->get_response_headers_array();
+ if ( !$incoming_headers )
+ break;
+ if ( isset($incoming_headers['Location']) )
+ {
+ // we've been redirected...
+ $new_uri = $this->_resolve_uri($incoming_headers['Location']);
+ if ( !$new_uri )
+ {
+ // ... bad URI, ignore Location header.
+ break;
+ }
+ // change location
+ $this->host = $new_uri['host'];
+ $this->port = $new_uri['port'];
+ $this->uri = $new_uri['uri'];
+ $get = '';
+
+ // reset
+ $this->sock_close($this->socket);
+ $this->_sock_open($this->socket);
+ $this->_write_request($this->socket, $headers, $cookies, $get, $post);
+ $buffer = $this->_read_until_newlines($this->socket);
+ $this->state = 3;
+ $this->_parse_response_code($buffer);
+ $this->response = $buffer;
+ $i++;
+ }
+ else
+ {
+ break;
+ }
+ }
+ if ( $i == 20 )
+ {
+ throw new Exception(__METHOD__ . ": Redirect trap. Request_HTTP doesn't do cookies, btw.");
+ }
+
+ if ( $this->state == 3 )
+ {
+ // Determine transfer encoding
+ $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
+ if ( preg_match("/^Content-Length: ([0-9]+)[\s]*$/mi", $this->response, $match) && !$is_chunked )
+ {
+ $size = intval($match[1]);
+ if ( $this->debug )
+ {
+ echo "Pulling response using fread(), size $size\n";
+ }
+ $this->response .= fread($this->socket, $size);
+ }
+ else
+ {
+ if ( $this->debug )
+ echo "Pulling response using chunked handler\n";
+
+ $buffer = '';
+ while ( !feof($this->socket) )
+ {
+ $part = fgets($this->socket, 1024);
+ if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
+ {
+ $chunklen = hexdec($match[1]);
+ $part = ( $chunklen > 0 ) ? fread($this->socket, $chunklen) : '';
+ // remove the last newline from $part
+ $part = preg_replace("/\r?\n\$/", "", $part);
+ }
+ $buffer .= $part;
+ }
+ $this->response .= $buffer;
+ }
+ }
+ $this->state = 4;
+
+ $this->sock_close($this->socket);
+ $this->socket = false;
+ }
+
+ /**
+ * Internal function to send the request but only fetch the headers. Leaves a connection open for a finish-up function.
+ * @access private
+ */
+
+ function _send_request_headers_only()
+ {
+ $this->concat_headers($headers, $cookies, $get, $post);
+
+ if ( $this->state < 1 )
+ {
+ $this->_sock_open($this->socket);
+ }
+ if ( $this->state < 2 )
+ {
+ $this->_write_request($this->socket, $headers, $cookies, $get, $post);
+ }
+ if ( $this->state == 2 )
+ {
+ $buffer = $this->_read_until_newlines($this->socket);
+ $this->state = 3;
+ $this->_parse_response_code($buffer);
+ $this->response = $buffer;
+ }
+ }
+
+ /**
+ * Internal function to read from a socket until two consecutive newlines are hit.
+ * @access private
+ */
+
+ function _read_until_newlines($sock)
+ {
+ $prev_char = '';
+ $prev1_char = '';
+ $prev2_char = '';
+ $buf = '';
+ while ( !feof($sock) )
+ {
+ $chr = fread($sock, 1);
+ $buf .= $chr;
+ if ( ( $chr == "\n" && $prev_char == "\n" ) ||
+ ( $chr == "\n" && $prev_char == "\r" && $prev1_char == "\n" && $prev2_char == "\r" ) )
+ {
+ return $buf;
+ }
+ $prev2_char = $prev1_char;
+ $prev1_char = $prev_char;
+ $prev_char = $chr;
+ }
+ return $buf;
+ }
+
+ /**
+ * Returns the response text. If the request hasn't been sent, it will be sent here.
+ * @return string
+ */
+
+ function get_response()
+ {
+ if ( $this->state == 4 )
+ return $this->response;
+ $this->_send_request();
+ return $this->response;
+ }
+
+ /**
+ * Writes the response body to a file. This is good for conserving memory when downloading large files. If the file already exists it will be overwritten.
+ * @param string File to write to
+ * @param int Chunk size in KB to read from the socket. Optional and should only be needed in circumstances when extreme memory conservation is needed. Defaults to 768.
+ * @param int Maximum file size. Defaults to 0, which means no limit.
+ * @return bool True on success, false on failure
+ */
+
+ function write_response_to_file($file, $chunklen = 768, $max_file_size = 0)
+ {
+ if ( !is_writeable( dirname($file) ) || !file_exists( dirname($file) ) )
+ {
+ return false;
+ }
+ $handle = @fopen($file, 'w');
+ if ( !$handle )
+ return false;
+ $chunklen = intval($chunklen);
+ if ( $chunklen < 1 )
+ return false;
+ if ( $this->state == 4 )
+ {
+ // we already have the response, so cheat
+ $response = $this->get_response_body();
+ fwrite($handle, $response);
+ }
+ else
+ {
+ // read data from the socket, write it immediately, and unset to free memory
+ $headers = $this->get_response_headers();
+ $transferred_bytes = 0;
+ $bandwidth_exceeded = false;
+ // if transfer-encoding is chunked, read using chunk sizes the server specifies
+ $is_chunked = preg_match("/Transfer-Encoding: (chunked)\r?\n/", $this->response);
+ if ( $is_chunked )
+ {
+ $buffer = '';
+ while ( !feof($this->socket) )
+ {
+ $part = fgets($this->socket, ( 1024 * $chunklen ));
+ // Theoretically if the encoding is really chunked then this should always match.
+ if ( $is_chunked && preg_match("/^([a-f0-9]+)\x0D\x0A$/", $part, $match) )
+ {
+ $chunk_length = hexdec($match[1]);
+ $part = ( $chunk_length > 0 ) ? fread($this->socket, $chunk_length) : '';
+ // remove the last newline from $part
+ $part = preg_replace("/\r?\n\$/m", "", $part);
+ }
+
+ $transferred_bytes += strlen($part);
+ if ( $max_file_size && $transferred_bytes > $max_file_size )
+ {
+ // truncate output to $max_file_size bytes
+ $partlen = $max_file_size - ( $transferred_bytes - strlen($part) );
+ $part = substr($part, 0, $partlen);
+ $bandwidth_exceeded = true;
+ }
+ fwrite($handle, $part);
+ if ( $bandwidth_exceeded )
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ $first_chunk = fread($this->socket, ( 1024 * $chunklen ));
+ fwrite($handle, $first_chunk);
+ while ( !feof($this->socket) )
+ {
+ $chunk = fread($this->socket, ( 1024 * $chunklen ));
+
+ $transferred_bytes += strlen($chunk);
+ if ( $max_file_size && $transferred_bytes > $max_file_size )
+ {
+ // truncate output to $max_file_size bytes
+ $partlen = $max_file_size - ( $transferred_bytes - strlen($chunk) );
+ $chunk = substr($chunk, 0, $partlen);
+ $bandwidth_exceeded = true;
+ }
+
+ fwrite($handle, $chunk);
+ unset($chunk);
+
+ if ( $bandwidth_exceeded )
+ {
+ break;
+ }
+ }
+ }
+ }
+ fclose($handle);
+ // close socket and reset state, since we haven't cached the response
+ $this->sock_close($this->socket);
+ $this->state = 0;
+ return ($bandwidth_exceeded) ? false : true;
+ }
+
+ /**
+ * Resolves, based on current settings and URI, a URI string to an array consisting of a host, port, and new URI. Returns false on error.
+ * @param string
+ * @return array
+ */
+
+ function _resolve_uri($uri)
+ {
+ // long ass regexp w00t
+ if ( !preg_match('#^(?:https?://((?:(?:[a-z0-9-]+\.)*)(?:[a-z0-9-]+)|\[[a-f0-9:]+\])(?::([0-9]+))?)?(/)(.*)$#i', $uri, $match) )
+ {
+ // bad target URI
+ return false;
+ }
+ $hostpart = $match[1];
+ if ( empty($hostpart) )
+ {
+ // use existing host
+ $host = $this->host;
+ $port = $this->port;
+ }
+ else
+ {
+ $host = $match[1];
+ $port = empty($match[2]) ? 80 : intval($match[2]);
+ }
+ // is this an absolute URI, or relative?
+ if ( empty($match[3]) )
+ {
+ // relative
+ $uri = dirname($this->uri) . $match[4];
+ }
+ else
+ {
+ // absolute
+ $uri = '/' . $match[4];
+ }
+ return array(
+ 'host' => $host,
+ 'port' => $port,
+ 'uri' => $uri
+ );
+ }
+
+ /**
+ * Returns only the response headers.
+ * @return string
+ */
+
+ function get_response_headers()
+ {
+ if ( $this->state == 3 )
+ {
+ return $this->response;
+ }
+ else if ( $this->state == 4 )
+ {
+ $pos_end = strpos($this->response, "\r\n\r\n");
+ if ( empty($pos_end) )
+ {
+ $pos_end = strpos($this->response, "\n\n");
+ }
+ $data = substr($this->response, 0, $pos_end);
+ return $data;
+ }
+ else
+ {
+ $this->_send_request_headers_only();
+ return $this->response;
+ }
+ }
+
+ /**
+ * Returns only the response headers, as an associative array.
+ * @return array
+ */
+
+ function get_response_headers_array()
+ {
+ $data = $this->get_response_headers();
+ preg_match_all("/(^|\n)([A-z0-9_-]+?): (.+?)(\r|\n|\$)/", $data, $matches);
+ $headers = array();
+ for ( $i = 0; $i < count($matches[0]); $i++ )
+ {
+ $headers[ $matches[2][$i] ] = $matches[3][$i];
+ }
+ return $headers;
+ }
+
+ /**
+ * Returns only the body (not the headers) of the response. If the request hasn't been sent, it will be sent here.
+ * @return string
+ */
+
+ function get_response_body()
+ {
+ $data = $this->get_response();
+ $pos_start = strpos($data, "\r\n\r\n") + 4;
+ if ( $pos_start == 4 )
+ {
+ $pos_start = strpos($data, "\n\n") + 4;
+ }
+ $data = substr($data, $pos_start);
+ return $data;
+ }
+
+ /**
+ * Returns all cookies requested to be set by the server as an associative array. If the request hasn't been sent, it will be sent here.
+ * @return array
+ */
+
+ function get_cookies()
+ {
+ $data = $this->get_response();
+ $data = str_replace("\r\n", "\n", $data);
+ $pos = strpos($data, "\n\n");
+ $headers = substr($data, 0, $pos);
+ preg_match_all("/Set-Cookie: ([a-z0-9_]+)=([^;]+);( expires=([^;]+);)?( path=(.*?))?\n/", $headers, $cookiematch);
+ if ( count($cookiematch[0]) < 1 )
+ return array();
+ $cookies = array();
+ foreach ( $cookiematch[0] as $i => $cookie )
+ {
+ $cookies[$cookiematch[1][$i]] = $cookiematch[2][$i];
+ }
+ return $cookies;
+ }
+
+ /**
+ * Internal method to write data to a socket with debugging possibility.
+ * @access private
+ */
+
+ function _fputs($socket, $data)
+ {
+ if ( $this->debug )
+ echo htmlspecialchars($data);
+
+ return fputs($socket, $data);
+ }
+
+ /**
+ * Internal function to stringify cookies, headers, get, and post.
+ * @access private
+ */
+
+ function concat_headers(&$headers, &$cookies, &$get, &$post)
+ {
+ $headers = '';
+ $cookies = '';
+ foreach ( $this->headers as $name => $value )
+ {
+ $value = str_replace('\\n', '\\\\n', $value);
+ $value = str_replace("\n", '\\n', $value);
+ $headers .= "$name: $value\r\n";
+ }
+ unset($value);
+ if ( count($this->cookies_out) > 0 )
+ {
+ $i = 0;
+ $cookie_header = 'Cookie: ';
+ foreach ( $this->cookies_out as $name => $value )
+ {
+ $i++;
+ if ( $i > 1 )
+ $cookie_header .= '; ';
+ $value = str_replace(';', rawurlencode(';'), $value);
+ $value = str_replace('\\n', '\\\\n', $value);
+ $value = str_replace("\n", '\\n', $value);
+ $cookie_header .= "$name=$value";
+ }
+ $cookie_header .= "\r\n";
+ $cookies = $cookie_header;
+ unset($value, $cookie_header);
+ }
+ if ( count($this->parms_get) > 0 )
+ {
+ $get = '?';
+ $i = 0;
+ foreach ( $this->parms_get as $name => $value )
+ {
+ $i++;
+ if ( $i > 1 )
+ $get .= '&';
+ $value = urlencode($value);
+ if ( !empty($value) || is_string($value) )
+ $get .= "$name=$value";
+ else
+ $get .= "$name";
+ }
+ }
+ if ( count($this->parms_post) > 0 )
+ {
+ $post = '';
+ $i = 0;
+ foreach ( $this->parms_post as $name => $value )
+ {
+ $i++;
+ if ( $i > 1 )
+ $post .= '&';
+ $value = urlencode($value);
+ $post .= "$name=$value";
+ }
+ }
+ }
+
}