<?php
/**
 * Simple HTTP request fallback system
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 * @author Jacob Santos <wordpress@santosj.name>
 */

/**
 * Abstract class for all of the fallback implementation
 * classes. The implementation classes will extend this class
 * to keep common API methods universal between different
 * functionality.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 * @abstract
 */
class WP_HTTP_Base
{
	/**
	 * The timeout variable that will be used to set for the
	 * fallback HTTP retrieval classes. Default is 30 seconds.
	 *
	 * @var integer
	 * @access protected
	 */
	var $timeout = 30;

	/**
	 * Stores the Headers as an array for easy retrieval by header name.
	 *
	 * @var array
	 * @access protected
	 */
	var $headers = null;

	/**
	 * Stores the entire body string.
	 *
	 * @var string
	 * @access protected
	 */
	var $body = null;

	/**
	 * The error, if any, that occurred while trying to retrieve the URL.
	 *
	 * Will be the array, with the key as the HTTP response code and string of response.
	 *
	 * @var array
	 * @access protected
	 */
	var $error = null;

	/**
	 * Uses the POST HTTP method.
	 * 
	 * Used for sending data that is expected to be in the body.
	 *
	 * @access public
	 * @param string $url The location of the site and page to retrieve.
	 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
	 * @param string $body Optional. The body that should be sent. Expected to be already processed.
	 * @return boolean
	 */
	function post($url, $headers=null, $body=null)
	{
		return $this->request($url, 'POST', $headers, $body);
	}

	/**
	 * Uses the GET HTTP method. 
	 *
	 * Used for sending data that is expected to be in the body.
	 *
	 * @access public
	 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
	 * @param string $body Optional. The body that should be sent. Expected to be already processed.
	 * @return boolean
	 */
	function get($url, $headers=null, $body=null)
	{
		return $this->request($url, 'GET', $headers, $body);
	}

	/**
	 * Set the headers.
	 *
	 * Should only be used before sending the request for the URL retrieval. Setting it after will
	 * overwrite any headers that might have been processed.
	 *
	 * @access public
	 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
	 */
	function setHeaders($headers)
	{
		if( is_numeric($headers) || is_object($headers) ) {
			throw trigger_error('$headers variable must be of type string or array', E_USER_WARNING);
			return;
		}

		$this->headers = $headers;
	}

	/**
	 * Get the headers from the retrieved site.
	 *
	 * Should be used after the request for the location. Default is to return the full list as an array.
	 *
	 * If $header is used, then the value from the header matching $header will be returned. if
	 * no header exists with that matches, an empty string will be returned instead.
	 *
	 * @access public
	 * @param string $header Optional. The header name to return.
	 * @return string|array If $header is used, will return value of header, else will return the whole headers list.
	 */
	function getHeaders($header=null)
	{
		if( !is_array( $this->headers ) ) {
			$arrTempHeaderList = explode("\n", str_replace("\r", '', $this->headers) );

			$this->headers = array();
			foreach($arrTempHeaderList as $header) {
				list($key, $value) = explode(":", $header, 2);
				$this->headers[$key] = trim($value);
			}
		}

		if( !is_null($header) && isset($this->headers[$header]) )
			return $this->headers[$header];

		return $this->headers;
	}

	/**
	 * Sets the body for sending to the retrieved site.
	 *
	 * Should only be used before retrieving the site. Setting the body after doing so
	 * will overwrite whatever was retrieved from the page.
	 *
	 * The body should be already processed for submitting to the site.
	 *
	 * @access public
	 * @param string $body The processed string to be submitted after the headers to the site.
	 */
	function setBody($body)
	{
		if( is_string($body) )
			$this->body = $body;
	}

	/**
	 * Gets the body from the retrieved site.
	 *
	 * @access public
	 * @return null|string If location is retrieved, will always return string. If not, then null will be returned.
	 */
	function getBody()
	{
		return $this->body;
	}
	
	/**
	 * Sets the timeout
	 *
	 * Has a filter applied so that plugins may set the timeout. The filter tag is
	 * 'http_request_timeout' and has no default hooks.
	 *
	 * @access public
	 * @param int $timeout The amount of seconds to try the url before failing
	 */
	function setTimeout($timeout)
	{
		$this->timeout = apply_filters('http_request_timeout', intval($timeout) );
	}

	/**
	 * Whether an error occurred during processing the URL request.
	 *
	 * @access public
	 * @return boolean
	 */
	function hasError()
	{
		return is_null($this->error);
	}

	/**
	 * Retrieve the error, if any, that might have occurred during processing
	 * the URL request.
	 *
	 * @access public
	 * @return null|WP_Error If error occurred, will be WP_Error, else will be null if no error.
	 */
	function getError()
	{
		reset($this->error);

		if( !$this->hasError() )
			return WP_Error( key($this->error), current($this->error) );
		
		return null;
	}

	/**
	 * Retrieve the location and set the class properties after the site has been retrieved.
	 *
	 * @abstract
	 * @access public
	 * @param string $url
	 * @param string $type Optional. Should be of HTTP Request type: GET, POST, HEAD.
	 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
	 * @param string $body Optional. The body that should be sent. Expected to be already processed.
	 * @return boolean
	 */
	function request($url, $type='GET', $headers=null, $body=null) {
		trigger_error('Class does not implement request method!', E_USER_ERROR);
	}

	/**
	 * Will test to make sure that the HTTP retrieval method is available for use.
	 *
	 * @abstract
	 * @access public
	 * @return boolean Whether the method can be used.
	 */
	function test() {
		trigger_error('Class does not implement test method!', E_USER_ERROR);
	}
}

/**
 * HTTP request method uses fsockopen function to retrieve the url.
 *
 * Preferred method since it works with all WordPress supported PHP versions.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 */
class WP_HTTP_Fsockopen extends WP_HTTP_Base
{
	/**
	 * Enter description here...
	 *
	 * @access public
	 * @param string $url
	 * @param string $type Optional. Should be of HTTP Request type: GET, POST, HEAD.
	 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
	 * @param string $body Optional. The body that should be sent. Expected to be already processed.
	 * @return boolean
	 */
	function request($url, $type='GET', $headers=null, $body=null)
	{
		
	}

	/**
	 * Whether this class can be used for retrieving an URL.
	 *
	 * @return boolean False means this class can not be used, true means it can.
	 */
	function test()
	{
		
	}
}

/**
 * HTTP request method uses fopen function to retrieve the url.
 *
 * Requires PHP version greater than 4.3.0 for stream support. Does not allow
 * for $context support, but should still be okay, to write the headers, before
 * getting the response. Also requires that 'allow_url_fopen' to be enabled.
 *
 * Second preferred method for handling the retrieving the url for PHP 4.3+.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 */
class WP_HTTP_Fopen extends WP_HTTP_Base
{
	/**
	 * Whether this class can be used for retrieving an URL.
	 *
	 * @return boolean False means this class can not be used, true means it can.
	 */
	function test()
	{
		if( false === ini_get('allow_url_fopen') )
			return false;

		// Make the assumption that Streams could fail, or that the preference could change
		// in the future for which goes first.
		if( version_compare(PHP_VERSION, '4.3', '<') )
			return false;
		
		return true;
	}
}

/**
 * HTTP request method uses cURL PHP extension to retrieve the url.
 *
 * Requires that cURL extension be installed. If extension is found, then
 * it should be assumed that it can be used to get retrieve the url.
 *
 * Third preferred method for getting the URL, after Streams and fopen.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 */
class WP_HTTP_Curl extends WP_HTTP_Base
{
	/**
	 * Whether this class can be used for retrieving an URL.
	 *
	 * @return boolean False means this class can not be used, true means it can.
	 */
	function test()
	{
		if( function_exists('curl_init') )
			return true;

		return false;
	}
}

/**
 * HTTP request method uses Streams to retrieve the url.
 *
 * Requires PHP 5.0+ and uses fopen with stream context. Requires that
 * 'allow_url_fopen' PHP setting to be enabled.
 *
 * Second preferred method for getting the URL, for PHP 5.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 */
class WP_HTTP_Streams extends WP_HTTP_Base
{
	/**
	 * Whether this class can be used for retrieving an URL.
	 *
	 * @return boolean False means this class can not be used, true means it can.
	 */
	function test()
	{
		if( false === ini_get('allow_url_fopen') )
			return false;

		if( version_compare(PHP_VERSION, '5.0', '<') )
			return false;
		
		return true;
	}
}

/**
 * HTTP request method uses HTTP extension to retrieve the url.
 *
 * Requires the HTTP extension to be installed.
 *
 * Last ditch effort to retrieve the URL before complete failure.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 */
class WP_HTTP_ExtHTTP extends WP_HTTP_Base
{
	/**
	 * Whether this class can be used for retrieving an URL.
	 *
	 * @return boolean False means this class can not be used, true means it can.
	 */
	function test()
	{
		if( function_exists('http_request') )
			return true;

		return false;
	}
}

/**
 * wp_remote_get_object() - Uses the working HTTP request object to retrieve URL.
 *
 * Sets up all of the HTTP Request objects until a working fallback has been
 * found and will use it to retrieve the URL.
 *
 * The global $wp_fetch_http_obj is used to allow for a plugin to define its own
 * object for retrieving URLs. It will also be used as an cache for reusing the same
 * object known to work, for all requests.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 *
 * @global object $wp_fetch_http_obj WP_HTTP_Base extended object for retrieving remote files.
 *
 * @param string $url Site URL to retrieve.
 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
 * @param string $body Optional. The body that should be sent. Expected to be already processed.
 * @param int $timeout Default is 30. The amount of time in seconds to continue trying to fetch the url before failing.
 * @return WP_HTTP_Base Extended class that has the response of the request.
 */
function wp_remote_get_object($url, $headers=null, $body=null, $timeout=30) {
	$wp_fetch_http_obj = _wp_http_get_object();

	// This filter should probably create and apply an array instead of applying
	// both strings and arrays. One would be best and it should be an array.
	$headers = apply_filters('http_request_headers', $headers);

	if( !is_null($headers) )
		$wp_fetch_http_obj->setHeaders($headers);

	if( !is_null($body) )
		$wp_fetch_http_obj->setBody($body);

	$wp_fetch_http_obj->setTimeout($timeout);

	$wp_fetch_http_obj->get($url);
	
	return $wp_fetch_http_obj;
}

/**
 * wp_remote_get_body() - Retrieve only the body from the URL.
 *
 * @param string $url Site URL to retrieve.
 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
 * @param string $body Optional. The body that should be sent. Expected to be already processed.
 * @param int $timeout Default is 30. The amount of time in seconds to continue trying to fetch the url before failing.
 * @return string The body of the response
 */
function wp_remote_get_body($url, $headers=null, $body=null, $timeout=30) {
	$objFetchSite = wp_remote_get_object($url, $headers, $body, $timeout);

	if($objFetchSite->hasError() === true)
		return $objFetchSite->getError();

	return $objFetchSite->getBody();
}

/**
 * wp_remote_get_headers() - Retrieve only the headers from the URL.
 *
 * @param string $url Site URL to retrieve.
 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
 * @param string $body Optional. The body that should be sent. Expected to be already processed.
 * @param int $timeout Default is 30. The amount of time in seconds to continue trying to fetch the url before failing.
 * @return array The headers of the response
 */
function wp_remote_get_headers($url, $headers=null, $body=null, $timeout=30) {
	$objFetchSite = wp_remote_get_object($url, $headers, $body, $timeout);

	if($objFetchSite->hasError() === true)
		return $objFetchSite->getError();

	return $objFetchSite->getHeaders();
}

/**
 * wp_remote_get() - Retrieve both the body and headers
 *
 * The body and headers will be split and returned in an array. The body will be first, with the headers
 * second.
 *
 * @param string $url Site URL to retrieve.
 * @param string|array $headers Optional. Either the header string or array of Header name and value pairs.
 * @param string $body Optional. The body that should be sent. Expected to be already processed.
 * @param int $timeout Default is 30. The amount of time in seconds to continue trying to fetch the url before failing.
 * @return array The body and the headers, in that order.
 */
function wp_remote_get($url, $headers=null, $body=null, $timeout=30) {
	$objFetchSite = wp_remote_get_object($url, $headers, $body, $timeout);
	
	if($objFetchSite->hasError() === true)
		return $objFetchSite->getError();

	return array($objFetchSite->getBody(), $objFetchSite->getHeaders());
}

/**
 * Tests the WordPress HTTP objects for an object to use and returns it.
 *
 * Tests all of the objects and returns the object that passes. Also caches that
 * object to be used later.
 *
 * @package WordPress
 * @subpackage HTTP
 * @since None
 * @access private
 *
 * @return WP_HTTP_Base|false False is failure to find working fallback object. 
 */
function _wp_http_get_object() {
	static $objHTTPRequest;

	// Object already found, return it.
	if( is_object($objHTTPRequest) )
		return $objHTTPRequest;

	// Find the working HTTP request object

	// Apply filter first in case plugin sets up another object
	$objHTTPRequest = apply_filters('http_request_use_custom_object', null);

	if( !is_null($objHTTPRequest) )
		return $objHTTPRequest;

	// Primary method to use, should pass most of the time
	$objHTTPRequest = new WP_HTTP_Fsockopen();

	if( true === $objHTTPRequest->test() )
		return $objHTTPRequest;

	// Second fallback
	$objHTTPRequest = new WP_HTTP_Streams();

	if( true === $objHTTPRequest->test() )
		return $objHTTPRequest;

	// Third fallback
	$objHTTPRequest = new WP_HTTP_Fopen();

	if( true === $objHTTPRequest->test() )
		return $objHTTPRequest;

	// Fourth fallback
	$objHTTPRequest = new WP_HTTP_Curl();

	if( true === $objHTTPRequest->test() )
		return $objHTTPRequest;

	// Final fallback
	$objHTTPRequest = new WP_HTTP_ExtHTTP();

	if( true === $objHTTPRequest->test() )
		return $objHTTPRequest;

	// No fallback was found.
	$objHTTPRequest = null;
	return false;
}

?>