<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flHttpClientCookie Class
 *
 * Class to hold a single cookie.
 *
 * @package fusionLib
 */
class flHttpClientCookie {

	/**
	 * Cookie name.
	 *
	 * @var string
	 */
	private $name;

	/**
	 * Cookie value.
	 *
	 * @var string
	 */
	private $value;

	/**
	 * Expire date of cookie.
	 *
	 * @var int
	 */
	private $expires;

	/**
	 * Domain cookie belongs to.
	 *
	 * @var string
	 */
	private $domain;

	/**
	 * Path of cookie.
	 *
	 * @var string
	 */
	private $path;

	/**
	 * Flags if the cookie is for https only.
	 *
	 * @var string
	 */
	private $secure;

	/**
	 * Construct the cookie.
	 *
	 * @param string $name The name of the cookie.
	 * @param string $value The value for the cookie.
	 * @param string $domain The domain the cookie belongs to.
	 * @param int $expires The timestamp of when the cookie expires or null to never expire.
	 * @param string $path The path for the cookie.
	 * @param bool $secure true if the cookie should only be used over https connections or false.
	 */
	function __construct($name, $value, $domain, $expires = null, $path = null, $secure = false) {
		$this->name = $name;
		$this->value = $value;
		$this->domain = $domain;
		$this->expires = $expires;
		$this->path = $path ? $path : '/';
		$this->secure = $secure;
	}

	/**
	 * Parse the raw cookie data sent from the server and store the cookie.
	 *
	 * @param string $domain The current domain name
	 * @param string $cookie The raw cookie data
	 * @return flHttpClientCookie The cookie object for the data.
	 */
	static function parseCookie($domain, $cookie) {

		// The new cookie
		$c = new flHttpClientCookie('', '', $domain);

		// Break apart cookie
		$el = explode(';', $cookie);

		// Get the name + value
		$t = explode('=', $el[0], 2);
		$c->name = trim($t[0]);
		$c->value = urldecode(trim($t[1]));

		// Get the remaining data
		$l = count($el);
		for($i=1;$i<$l;$i++) {
			$t = explode('=', $el[$i], 2);
			if(count($t) == 2) {
				switch(trim($t[0])) {
					case 'expires':
						$c->expires = strtotime(trim($t[1]));
						break;
					case 'path':
						$c->path = trim($t[1]);
						break;
					case 'secure':
						$c->secure = true;
						break;
					case 'domain':
						$c->domain = trim($t[1]);
						break;
				}
			}
		}

		// Return the cookie
		return $c;
	}

	/**
	 * Test if the cookie has expired.
	 *
	 * @return bool true if the cookie has expired; else false.
	 */
	function isExpired() {
		return $this->expires !== null && $this->expires < time();
	}

	/**
	 * Get the domain name.
	 *
	 * @return string The domain name.
	 */
	function getDomain() {
		return $this->domain;
	}

	/**
	 * Get the time the cookie expires.
	 *
	 * @return int Unix timestamp of the time the cookie expires or null.
	 */
	function getExpires() {
		return $this->expires;
	}

	/**
	 * Get the name of the cookie.
	 *
	 * @return string The cookie name.
	 */
	function getName() {
		return $this->name;
	}

	/**
	 * Get the path for the cookie.
	 *
	 * @return string The path.
	 */
	function getPath() {
		return $this->path;
	}

	/**
	 * Get if the cookie is secure.
	 *
	 * @return bool true if the cookie is secure, else false.
	 */
	function getSecure() {
		return $this->secure;
	}

	/**
	 * Get the value of the cookie.
	 *
	 * @return string The value.
	 */
	function getValue() {
		return $this->value;
	}

	/**
	 * Get the cookie as a string suitable for sending to the server.
	 *
	 * @return string The cookie as a string.
	 */
	function __toString() {
		return $this->name . '=' . urlencode($this->value) . ';';
	}

	/**
	 * Tests if the cookie matches the given domain, path and secure settings.
	 *
	 * @param string $domain The domain to match against.
	 * @param string $path The path to match against.
	 * @param bool $secure The secure settings.
	 * @return bool true if the cookie matches the given path else false.
	 */
	function matches($domain, $path, $secure) {
		return ($secure || !$this->secure) &&
				preg_match('/' . preg_quote($this->domain, '/') . '$/', $domain) &&
				preg_match('/^' . preg_quote($this->path, '/') . '/', $path);

	}
}

/**
 * flHttpClient Class
 *
 * The default operation of GET is used if there is no data or files to POST.
 *
 * Although cookies are supported neither the path nor expire date is checked.
 *
 * See http://wp.netscape.com/newsref/std/cookie_spec.html for information on cookies
 *
 * @package fusionLib
 */
class flHttpClient {

	/**#@+
	 * Request method.
	 */
	const GET = 'GET';
	const POST  = 'POST';
	const PUT = 'PUT';
	const HEAD = 'HEAD';
	const DELETE = 'DELETE';
	const TRACE = 'TRACE';
	const OPTIONS = 'OPTIONS';
	const CONNECT = 'CONNECT';
	/**#@-*/

	/**
	 * The URI to access, this may contain parameters e.g. http://www.example.com/?a=1&b=3
	 *
	 * @var string
	 */
	private $uri = '';

	/**
	 * Array of custom headers, the array is indexed by the header e.g. 'User-Agent' => 'test agent'
	 *
	 * @var array
	 */
	private $headers;

	/**
	 * Associative array of key and values to use in a post operation.
	 *
	 * @var array
	 */
	private $postData = array();

	/**
	 * Associative array of key and values to use in the query string
	 * @var array
	 */
	private $queryData = array();

	/**
	 * Associative array of files to POST each entry is: name => array(mime type, file, data)
	 *
	 * @var array
	 */
	private $files = array();

	/**
	 * Array of cookies to send to the hosts and also those accepted from the remote server. The array is multidimensional
	 * with the 1st dimension being the host the 2nd is the path and the 3rd the cookie name, e.g. $cookies['www.fusionlib.com']['/']['example']
	 *
	 * @var array
	 */
	private $cookies = array();

	/**
	 * Flags if redirects should be followed or not.
	 *
	 * @var bool
	 */
	private $follow = true;

	/**
	 * Maximum size in bytes of the page well read back when using sockets.
	 *
	 * @var int
	 */
	private $maxSize = 1048576;

	/**
	 * The timeout to use.
	 *
	 * @var int
	 */
	private $timeout = 30;

	/**
	 * The username and password for basic authentication.
	 *
	 * @var string
	 */
	private $basicAuth = null;

	/**
	 * The content type.
	 *
	 * @var string
	 */
	private $contentType = 'application/x-www-form-urlencoded';

	/**
	 * The header from the last request.
	 *
	 * @var array
	 */
	private $responseHdrs = array();

	/**
	 * The response code form the last request.
	 *
	 * @var int
	 */
	private $responseCode = 404;

	/**
	 * The body from the last request.
	 *
	 * @var string
	 */
	private $body = '';

	/**
	 * Flags if cookies from the server should be ignored, the default is to accept them.
	 *
	 * @var bool
	 */
	private $ignoreCookies = false;

	/**
	 * Create a new request object.
	 *
	 * @param string $uri The URI to access.
	 */
	function __construct($uri = null) {
		if($uri !== null)
			$this->setUri($uri);
		$this->clearHeaders();
	}

	/**
	 * Set the timeout.
	 *
	 * @param int $timeout The timeout in seconds.
	 * @return flHttpClient This object.
	 */
	function setTimeout($timeout) {
		$this->timeout = $timeout;
		return $this;
	}

	/**
	 * Set the content type for the request if no file upload.
	 *
	 * @param string $contentType The content type.
	 * @return flHttpClient This object.
	 */
	function setContentType($contentType) {
		$this->contentType = $contentType;
		return $this;
	}

	/**
	 * Set the URI to access.
	 *
	 * @param string $uri The URI to access, this may contain a username and password.
	 * @return flHttpClient This object.
	 * @throws flHttpClientException
	 */
	function setUri($uri = null) {
		// Parse the URI so that we can remove usernames and passwords
		$web = @parse_url($uri);
		if(!isset($web['path'])) $web['path'] = '/';

		// If bad URI abort
		if(empty($web['scheme']) || empty($web['host']))
			throw new flHttpClientException('Malformed URI given.');

		// Rebuild the URI
		$this->uri = $web['scheme'] . '://' . $web['host'] . (isset($web['port']) ? ':' . $web['port'] : '') . $web['path'] . (isset($web['query']) ? '?' . $web['query'] : '') . (isset($web['fragment']) ? '#' . $web['fragment'] : '');

		// Build authentication information
		if(isset($web['user']) && isset($web['pass']))
			$this->basicAuth = $web['user'] . ':' . $web['pass'];

		return $this;
	}

	/**
	 * Set the maximum size of a page that will be accepted, this has no effect when storing
	 * to disk.
	 *
	 * @param int $size The maximum allowed page size in bytes or 0 to allow any size.
	 * @return flHttpClient This object.
	 */
	function setMaxSize($size) {
		$this->maxSize = $size;
		return $this;
	}

	/**
	 * Set the username and password to use for authentication.
	 *
	 * @param string $username The username to use.
	 * @param string $password The password to use.
	 * @return flHttpClient This object.
	 */
	function setAuth($username, $password) {
		$this->basicAuth = $username . ':' . $password;
		return $this;
	}

	/**
	 * Clear the authentication information.
	 * @return flHttpClient This object.
	 */
	function clearAuth() {
		$this->basicAuth = null;
		return $this;
	}

	/**
	 * Add a header field.
	 *
	 * @param string $header The header to add
	 * @param string $value The value to assign to the header.
	 * @return flHttpClient This object.
	 */
	function addHeader($header, $value) {
		$this->headers[$header] = $value;
		return $this;
	}

	/**
	 * Remove a previously added header.
	 *
	 * @param string $header The header to remove.
	 * @return flHttpClient This object.
	 */
	function removeHeader($header) {
		unset($this->headers[$header]);
		return $this;
	}

	/**
	 * Clear all headers.
	 * @return flHttpClient This object.
	 */
	function clearHeaders() {
		$this->headers = array();

		// Add the default header for gzip encoding
		$this->headers['Accept-Encoding'] = 'gzip';
		$this->headers['Accept'] = '*/*';

		return $this;
	}

	/**
	 * Add a value to post during the request.
	 *
	 * @param string $name The name of the value
	 * @param string $value The value to post.
	 * @return flHttpClient This object.
	 */
	function addPostData($name, $value) {
		$this->postData[$name] = $value;
		return $this;
	}

	/**
	 * Remove a post value.
	 *
	 * @param string $name The name of the post value to remove.
	 * @return flHttpClient This object.
	 */
	function removePostData($name) {
		unset($this->postData[$name]);
		return $this;
	}

	/**
	 * Clear all post data.
	 * @return flHttpClient This object.
	 */
	function clearPostData() {
		$this->postData = array();
		return $this;
	}

	/**
	 * Set the data to post during the request, all existing data is replaced.
	 *
	 * @param array $data Array of name value pairs for the data to send, or string of data only if not uploading files.
	 * @return flHttpClient This object.
	 */
	function setPostData($data) {
		$this->postData = $data;
		return $this;
	}

	/**
	 * Add a value to query string.
	 *
	 * @param string $name The name of the value
	 * @param string $value The value to add.
	 * @return flHttpClient This object.
	 */
	function addQueryData($name, $value) {
		$this->queryData[$name] = $value;
		return $this;
	}

	/**
	 * Remove a query value.
	 *
	 * @param string $name The name of the query value to remove.
	 * @return flHttpClient This object.
	 */
	function removeQueryData($name) {
		unset($this->queryData[$name]);
		return $this;
	}

	/**
	 * Clear all query data.
	 * @return flHttpClient This object.
	 */
	function clearQueryData() {
		$this->queryData = array();
		return $this;
	}

	/**
	 * Set the query string data to use during the request, all existing data is replaced.
	 *
	 * @param array $data Array of name value pairs for the data to send.
	 * @return flHttpClient This object.
	 */
	function setQueryData($data) {
		$this->queryData = $data;
		return $this;
	}

	/**
	 * Add a file to be posted.
	 *
	 * @param string $name The name to send the file as.
	 * @param string $file The path to the file to send in the post.
	 * @param string $mimeType The mime type of the file or null to autodetect from the file extension.
	 * @param string $data If present then this is sent as the file contents, if null then the contents are loaded from the file.
	 * @return flHttpClient This object.
	 */
	function addFile($name, $file, $mimeType = null, $data = null) {
		$this->files[$name] = array(is_null($mimeType) ? flMimeTypes::getType($file) : $mimeType, $file, $data);
		return $this;
	}

	/**
	 * Remove the named file.
	 *
	 * @param string $name The name of the file to remove.
	 * @return flHttpClient This object.
	 */
	function removeFile($name) {
		unset($this->files[$name]);
		return $this;
	}

	/**
	 * Remove all the files.
	 * @return flHttpClient This object.
	 */
	function clearFiles() {
		$this->files = array();
		return $this;
	}

	/**
	 * Flags if redirects should be followed.
	 *
	 * @param bool $follow True if redirects should be followed or false if not.
	 * @return flHttpClient This object.
	 */
	function followRedirects($follow) {
		$this->follow = $follow;
		return $this;
	}

	/**
	 * Add a cookie to use with requests.
	 *
	 * @param flHttpClientCookie $cookie The cookie.
	 * @return flHttpClient This object.
	 */
	function addCookie($cookie) {
		$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
		return $this;
	}

	/**
	 * Remove all cookies.
	 * @return flHttpClient This object.
	 */
	function clearCookies() {
		$this->cookies = array();
		return $this;
	}

	/**
	 * Get an array of the cookies set by the server.
	 *
	 * @return array Array of all cookies [domain][path][name] => flHttpClientCookie
	 */
	function getCookies() {
		return $this->cookies;
	}

	/**
	 * Set all cookies replacing any existing.
	 *
	 * @param array $cookies Array of cookies [domain][path][name] => flHttpClientCookie
	 * @return flHttpClient This object.
	 */
	function setCookies($cookies) {
		$this->cookies = $cookies;
		return $this;
	}

	/**
	 * Flags if cookies sent from the server should be ignored or accepted.
	 *
	 * @param bool $ingoreCookies True if cookies should be accepted or false to ignore them.
	 * @return flHttpClient This object.
	 */
	function ignoreCookies($ingoreCookies) {
		$this->clearCookies();
		$this->ignoreCookies = $ingoreCookies;
		return $this;
	}

	/**
	 * Internal function to make a cookie header for the given domain.
	 *
	 * @param string $domain The domain name to make the cookies for
	 * @param string $path The path to make the cookies for
	 * @param bool $secure Flags if the link is secure.
	 *
	 * @return string The cookie string or null if no cookies found for the domain name
	 */
	private function _makeCookies($domain, $path, $secure) {

		$s = '';
		$cookiesTmp = array();

		// Purge expired cookies and build list to send to server
		foreach($this->cookies as $c) {
			foreach($c as $cookies) {
				foreach($cookies as $cd) {
					/** @param flHttpClientCookie $cd */
					if(!$cd->isExpired()) {
						// Keep cookie
						$cookiesTmp[$cd->getDomain()][$cd->getPath()][$cd->getName()] = $cd;

						// If cookie matches then add to output
						if($cd->matches($domain, $path, $secure))
							$s .= $cd;
					}
				}
			}
		}
		$this->cookies = $cookiesTmp;

		return strlen($s) ? $s : null;
	}

	/**
	 * Send the request.
	 *
	 * @param string $method The method to use for the request of null to auto select POST or GET.
	 * @return bool true if the request was sent or false if an error occurred.
	 */
	function sendRequest($method = null) {
		// Initialise
		$this->responseHdrs = array();
		$this->body = '';
		$this->responseCode = 404;

		$uri = $this->uri;
		if(count($this->headers)) {
			$headers = array();
			foreach($this->headers as $n => $v)
				$headers[] = $n . ': ' . $v;
		}
		else
			$headers = array();

		// If User-Agent not set
		if(!isset($this->headers['User-Agent'])) {
			$headers[] = 'User-Agent: fusionLib / ' . fusionLib::getVersion() . ' (PHP)';
		}

		$attempts = $this->follow ? 0 :  100;
		$done = false;
		$post = null;
		$boundry = uniqid('----=_NextPart_');

		// Load the file data if any
		if(count($this->files)) {
			$s = "--$boundry";

			// Add all post data
			if(count($this->postData)) {
				foreach($this->postData as $k => $v) {
					$s .= "\r\nContent-Disposition: form-data; name=\"$k\"\r\n\r\n";
					$s .=  urlencode($v) . "\r\n--$boundry";
				}
			}

			// add the files
			foreach($this->files as $n => $f) {
				$s .= "\r\nContent-Disposition: form-data; name=\"$n\"; filename=\"" . basename($f[1]) . "\"\r\nContent-Type: $f[0]\r\n\r\n";
				$s .= ($f[2] === null ? file_get_contents($f[1]) : $f[2]) . "\r\n--$boundry";
			}
			$s .= "--\r\n";
			$post = $s;
		}
		// If has string post data
		else if(is_string($this->postData) && !empty($this->postData))
			$post = $this->postData;
		// If has post data
		else if(count($this->postData))
			$post = http_build_query($this->postData);

		// Select request method
		$method = ($method === null ? ($post === null ? 'GET' : 'POST') : strtoupper($method));

		do {
			// Parse the URI
			$web = parse_url($uri);
			if(!isset($web['path'])) $web['path'] = '/';

			// Get cookies for domain name
			if($cookies = $this->_makeCookies($web['host'], $web['path'], $web['scheme'] == 'https'))
				$headers[-1] = 'Cookie: ' . $cookies;
			else
				unset($headers[-1]);

			if(!isset($web['port'])) {
				$web['port'] = @$web['scheme'] == 'https' ? '443' : '80';
			}

			$ssl = @$web['scheme'] == 'https';

			// Connect host
			$errnum = 0;
			$errstr = $this->body = '';
			$fp = @fsockopen(($ssl ? 'ssl://' : '') . $web['host'], $web['port'], $errnum, $errstr, $this->timeout);

			// Might be self-signed so open socket then switch to secure
			if($ssl && !$fp) {
				$fp = @fsockopen('tcp://' . $web['host'], $web['port'], $errnum, $errstr, $this->timeout);
				if($fp === false)
					return false;

				stream_context_set_option($fp, 'ssl', 'verify_host', false);
				stream_context_set_option($fp, 'ssl', 'verify_peer', false);
				stream_context_set_option($fp, 'ssl', 'verify_peer_name', false);
				stream_context_set_option($fp, 'ssl', 'allow_self_signed', true);

				if(!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
					fclose($fp);
					return false;
				}
			}

			if($fp) {
        		socket_set_timeout($fp, $this->timeout, 0);

				// Request header
				$queryParams = count($this->queryData) ? (isset($web['query']) ? '&' : '?') . http_build_query($this->queryData) : '';
				$request = "$method $web[path]" . (isset($web['query']) ? '?' . $web['query'] : '') . "$queryParams HTTP/1.1\r\nHost: $web[host]\r\n";

				// Add any post headers
				if($post !== null) {
					if(!count($this->files))
						$request .= "Content-type: $this->contentType\r\n";
					else
						$request .= "Content-type: multipart/form-data; boundary=$boundry\r\n";
					$request .= "Content-length: ".strlen($post)."\r\n";
				}

				// Add authentication
				if($this->basicAuth !== null)
					$request .= 'Authorization: Basic ' . base64_encode($this->basicAuth) . "\r\n";

				// Add custom headers
				foreach($headers as $h)
					$request .= $h . "\r\n";

				// Close the connection or it is left open
				$request .= "Connection: close\r\n\r\n";

				// Add post data
				if($post !== null) $request .= $post . "\r\n\r\n";

				// Write the request to the server
				if(!@fwrite($fp, $request)) {
					fclose($fp);
					return false;
				}

				// Read back header
				do {
					while(!feof($fp) && !preg_match('/\\r\\n\\r\\n$/', $this->body)) {
						if(($read = @fgets($fp, 4096)) === false) {
							fclose($fp);
							return false;
						}
						$this->body .= $read;
					}

					// Get the headers
					$this->responseHdrs = array();
					$hh = preg_split("/\r\n|[\r\n]/", $this->body);
					foreach($hh as $h) {
						if(strlen($h)) {
							@list($k, $v) = explode(':', $h, 2);
							if(isset($v)) {
								$k = strtolower(trim($k));
								if($k == 'set-cookie') {
									if(!$this->ignoreCookies) {
										$cookie = flHttpClientCookie::parseCookie($web['host'], trim($v));
										$this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
									}
								}
								else
									$this->responseHdrs[$k] = trim($v);
							}
							else {
								$rt = array();
								if(preg_match('/^HTTP\/.*\s+(\d+)(?:\s+.*)?$/', trim($k), $rt))
									$this->responseCode = $rt[1];
							}
						}
					}

					$this->body = '';
				} while($this->responseCode == 100);

				// If chunked encoding
				if(isset($this->responseHdrs['transfer-encoding']) && $this->responseHdrs['transfer-encoding'] == 'chunked') {
					while(!feof($fp)) {
						$ac = 300;

						if($chunkSize = hexdec(@fgets($fp))) {

							// read chunk
							$rl = 0;
							while($rl < $chunkSize && $ac) {
								if(($data = fread($fp, $chunkSize - $rl)) === false) {
									fclose($fp);
									return false;
								}

								$this->body .= $data;
								$rl += strlen($data);
								$ac--;
							}
						}

						// Check max size
						if(!$ac || ($this->maxSize && strlen($this->body) > $this->maxSize)) {
							fclose($fp);
							return false;
						}
					}
				}
				else {

					// Read back page data
					while(!feof($fp)) {
						if(($d = @fread($fp, 4096)) === false) {
							fclose($fp);
							return false;
						}

						$this->body .= $d;

						// Check max size
						if($this->maxSize && strlen($this->body) > $this->maxSize) {
							fclose($fp);
							return false;
						}
					}
				}

				fclose($fp);
			}
			else
				return false;

			// Revert to plain GET request on redirection
			$post = null;
			$method = self::GET;

			// If following headers
			if($this->follow && isset($this->responseHdrs['location'])) {

				// If abs address
				if(preg_match('/^http/', $this->responseHdrs['location']))
					$uri = $this->responseHdrs['location'];
				// If starts with a /
				else if($this->responseHdrs['location'][0] == '/')
					$uri = $web['scheme'] . '://' . $web['host'] . (isset($web['port']) ? ':' . $web['port'] : '') . $this->responseHdrs['location'];
				// Else need to merge path
				else {
					$path = substr($web['path'], -1) != '/' ? dirname($web['path']) . '/' : $web['path'];

					// Deal with relative paths
					while(preg_match('#^\.\./#', $this->responseHdrs['location'])) {
						$path = preg_replace('#[^/]+/$#', '', $path);
						$this->responseHdrs['location'] = substr($this->responseHdrs['location'], 3);
					}

					$uri = $web['scheme'] . '://' . $web['host'] . (isset($web['port']) ? ':' . $web['port'] : '') . $path . $this->responseHdrs['location'];
				}
			}
			else
				$done = true;

		} while(!$done && $attempts++ < 5);

		// If gzip encoded page
		if($done && isset($this->responseHdrs['content-encoding']) && $this->responseHdrs['content-encoding'] == 'gzip' && strlen($this->body)) {

			// inflate
			if($this->maxSize)
				$this->body = @gzinflate(substr($this->body, 10), $this->maxSize); // strip 10 byte header and uncompress
			else
				$this->body = @gzinflate(substr($this->body, 10)); // strip 10 byte header and uncompress

			// If failed to uncompress
			if($this->body === false) {
				// If using gzip encoding
				if(isset($this->headers['Accept-Encoding']) && $this->headers['Accept-Encoding'] == 'gzip') {
					// Disable gzip encoding and run request again
					unset($this->headers['Accept-Encoding']);
					$done = $this->sendRequest();
					$this->headers['Accept-Encoding'] = 'gzip';
				}
				else {
					$this->body = '';
					$done = false;
				}
			}
		}

		// Return page
		return $done;
	}

	/**
	 * Return the page body loaded from the last request.
	 *
	 * @return string The page body.
	 */
	function getBody() {
		return $this->body;
	}

	/**
	 * Get the response code from the last request.
	 *
	 * @return int The response code.
	 */
	function getResponseCode() {
		return $this->responseCode;
	}

	/**
	 * Get the response headers from the last request.
	 *
	 * @return array Array of headers.
	 */
	function getResponseHeaders() {
		return $this->responseHdrs;
	}
}
