<?php /** * base include file for SimpleTest * @package SimpleTest * @subpackage WebTester * @version $Id: http.php,v 1.98 2005/02/02 23:25:23 lastcraft Exp $ */ /**#@+ * include other SimpleTest class files */ require_once(dirname(__FILE__) . '/socket.php'); require_once(dirname(__FILE__) . '/url.php'); /**#@-*/ /** * Cookie data holder. Cookie rules are full of pretty * arbitary stuff. I have used... * http://wp.netscape.com/newsref/std/cookie_spec.html * http://www.cookiecentral.com/faq/ * @package SimpleTest * @subpackage WebTester */ class SimpleCookie { protected $_host; protected $_name; protected $_value; protected $_path; protected $_expiry; protected $_is_secure; /** * Constructor. Sets the stored values. * @param string $name Cookie key. * @param string $value Value of cookie. * @param string $path Cookie path if not host wide. * @param string $expiry Expiry date as string. * @param boolean $is_secure Currently ignored. */ function SimpleCookie($name, $value = false, $path = false, $expiry = false, $is_secure = false) { $this->_host = false; $this->_name = $name; $this->_value = $value; $this->_path = ($path ? $this->_fixPath($path) : "/"); $this->_expiry = false; if (is_string($expiry)) { $this->_expiry = strtotime($expiry); } elseif (is_integer($expiry)) { $this->_expiry = $expiry; } $this->_is_secure = $is_secure; } /** * Sets the host. The cookie rules determine * that the first two parts are taken for * certain TLDs and three for others. If the * new host does not match these rules then the * call will fail. * @param string $host New hostname. * @return boolean True if hostname is valid. * @access public */ function setHost($host) { if ($host = $this->_truncateHost($host)) { $this->_host = $host; return true; } return false; } /** * Accessor for the truncated host to which this * cookie applies. * @return string Truncated hostname. * @access public */ function getHost() { return $this->_host; } /** * Test for a cookie being valid for a host name. * @param string $host Host to test against. * @return boolean True if the cookie would be valid * here. */ function isValidHost($host) { return ($this->_truncateHost($host) === $this->getHost()); } /** * Extracts just the domain part that determines a * cookie's host validity. * @param string $host Host name to truncate. * @return string Domain or false on a bad host. * @access private */ function _truncateHost($host) { $tlds = SimpleUrl::getAllTopLevelDomains(); if (preg_match('/[a-z\-]+\.(' . $tlds . ')$/i', $host, $matches)) { return $matches[0]; } elseif (preg_match('/[a-z\-]+\.[a-z\-]+\.[a-z\-]+$/i', $host, $matches)) { return $matches[0]; } return false; } /** * Accessor for name. * @return string Cookie key. * @access public */ function getName() { return $this->_name; } /** * Accessor for value. A deleted cookie will * have an empty string for this. * @return string Cookie value. * @access public */ function getValue() { return $this->_value; } /** * Accessor for path. * @return string Valid cookie path. * @access public */ function getPath() { return $this->_path; } /** * Tests a path to see if the cookie applies * there. The test path must be longer or * equal to the cookie path. * @param string $path Path to test against. * @return boolean True if cookie valid here. * @access public */ function isValidPath($path) { return (strncmp( $this->_fixPath($path), $this->getPath(), strlen($this->getPath())) == 0); } /** * Accessor for expiry. * @return string Expiry string. * @access public */ function getExpiry() { if (! $this->_expiry) { return false; } return gmdate("D, d M Y H:i:s", $this->_expiry) . " GMT"; } /** * Test to see if cookie is expired against * the cookie format time or timestamp. * Will give true for a session cookie. * @param integer/string $now Time to test against. Result * will be false if this time * is later than the cookie expiry. * Can be either a timestamp integer * or a cookie format date. * @access public */ function isExpired($now) { if (! $this->_expiry) { return true; } if (is_string($now)) { $now = strtotime($now); } return ($this->_expiry < $now); } /** * Ages the cookie by the specified number of * seconds. * @param integer $interval In seconds. * @public */ function agePrematurely($interval) { if ($this->_expiry) { $this->_expiry -= $interval; } } /** * Accessor for the secure flag. * @return boolean True if cookie needs SSL. * @access public */ function isSecure() { return $this->_is_secure; } /** * Adds a trailing and leading slash to the path * if missing. * @param string $path Path to fix. * @access private */ function _fixPath($path) { if (substr($path, 0, 1) != '/') { $path = '/' . $path; } if (substr($path, -1, 1) != '/') { $path .= '/'; } return $path; } } /** * Creates HTTP headers for the end point of * a HTTP request. * @package SimpleTest * @subpackage WebTester */ class SimpleRoute { protected $_url; /** * Sets the target URL. * @param SimpleUrl $url URL as object. * @access public */ function SimpleRoute($url) { $this->_url = $url; } /** * Resource name. * @return SimpleUrl Current url. * @access protected */ function getUrl() { return $this->_url; } /** * Creates the first line which is the actual request. * @param string $method HTTP request method, usually GET. * @return string Request line content. * @access protected */ function _getRequestLine($method) { return $method . ' ' . $this->_url->getPath() . $this->_url->getEncodedRequest() . ' HTTP/1.0'; } /** * Creates the host part of the request. * @return string Host line content. * @access protected */ function _getHostLine() { $line = 'Host: ' . $this->_url->getHost(); if ($this->_url->getPort()) { $line .= ':' . $this->_url->getPort(); } return $line; } /** * Opens a socket to the route. * @param string $method HTTP request method, usually GET. * @param integer $timeout Connection timeout. * @return SimpleSocket New socket. * @access public */ function createConnection($method, $timeout) { $default_port = ('https' == $this->_url->getScheme()) ? 443 : 80; $socket = $this->_createSocket( $this->_url->getScheme() ? $this->_url->getScheme() : 'http', $this->_url->getHost(), $this->_url->getPort() ? $this->_url->getPort() : $default_port, $timeout); if (! $socket->isError()) { $socket->write($this->_getRequestLine($method) . "\r\n"); $socket->write($this->_getHostLine() . "\r\n"); $socket->write("Connection: close\r\n"); } return $socket; } /** * Factory for socket. * @param string $scheme Protocol to use. * @param string $host Hostname to connect to. * @param integer $port Remote port. * @param integer $timeout Connection timeout. * @return SimpleSocket/SimpleSecureSocket New socket. * @access protected */ function _createSocket($scheme, $host, $port, $timeout) { if (in_array($scheme, array('https'))) { return new SimpleSecureSocket($host, $port, $timeout); } return new SimpleSocket($host, $port, $timeout); } } /** * Creates HTTP headers for the end point of * a HTTP request via a proxy server. * @package SimpleTest * @subpackage WebTester */ class SimpleProxyRoute extends SimpleRoute { protected $_proxy; protected $_username; protected $_password; /** * Stashes the proxy address. * @param SimpleUrl $url URL as object. * @param string $proxy Proxy URL. * @param string $username Username for autentication. * @param string $password Password for autentication. * @access public */ function SimpleProxyRoute($url, $proxy, $username = false, $password = false) { $this->SimpleRoute($url); $this->_proxy = $proxy; $this->_username = $username; $this->_password = $password; } /** * Creates the first line which is the actual request. * @param string $method HTTP request method, usually GET. * @param SimpleUrl $url URL as object. * @return string Request line content. * @access protected */ function _getRequestLine($method) { $url = $this->getUrl(); $scheme = $url->getScheme() ? $url->getScheme() : 'http'; $port = $url->getPort() ? ':' . $url->getPort() : ''; return $method . ' ' . $scheme . '://' . $url->getHost() . $port . $url->getPath() . $url->getEncodedRequest() . ' HTTP/1.0'; } /** * Creates the host part of the request. * @param SimpleUrl $url URL as object. * @return string Host line content. * @access protected */ function _getHostLine() { $host = 'Host: ' . $this->_proxy->getHost(); $port = $this->_proxy->getPort() ? $this->_proxy->getPort() : 8080; return "$host:$port"; } /** * Opens a socket to the route. * @param string $method HTTP request method, usually GET. * @param integer $timeout Connection timeout. * @return SimpleSocket New socket. * @access public */ function createConnection($method, $timeout) { $socket = $this->_createSocket( $this->_proxy->getScheme() ? $this->_proxy->getScheme() : 'http', $this->_proxy->getHost(), $this->_proxy->getPort() ? $this->_proxy->getPort() : 8080, $timeout); if (! $socket->isError()) { $socket->write($this->_getRequestLine($method) . "\r\n"); $socket->write($this->_getHostLine() . "\r\n"); if ($this->_username && $this->_password) { $socket->write('Proxy-Authorization: Basic ' . base64_encode($this->_username . ':' . $this->_password) . "\r\n"); } $socket->write("Connection: close\r\n"); } return $socket; } } /** * HTTP request for a web page. Factory for * HttpResponse object. * @package SimpleTest * @subpackage WebTester */ class SimpleHttpRequest { protected $_route; protected $_method; protected $_encoding; protected $_headers; protected $_cookies; /** * Saves the URL ready for fetching. * @param SimpleRoute $route Request route. * @param string $method HTTP request method, * usually GET. * @param SimpleFormEncoding $encoding Content to send with * request or false. * @access public */ function SimpleHttpRequest($route, $method, $encoding = false) { $this->_route = $route; $this->_method = $method; $this->_encoding = $encoding; $this->_headers = array(); $this->_cookies = array(); } /** * Fetches the content and parses the headers. * @param integer $timeout Connection timeout. * @return SimpleHttpResponse A response which may only have * an error. * @access public */ function fetch($timeout) { $socket = $this->_route->createConnection($this->_method, $timeout); if ($socket->isError()) { return $this->_createResponse($socket); } $this->_dispatchRequest($socket, $this->_method, $this->_encoding); return $this->_createResponse($socket); } /** * Sends the headers. * @param SimpleSocket $socket Open socket. * @param string $method HTTP request method, * usually GET. * @param SimpleFormEncoding $encoding Content to send with request. * @access private */ function _dispatchRequest($socket, $method, $encoding) { if ($encoding || ($method == 'POST')) { $socket->write("Content-Length: " . $this->_getContentLength($encoding) . "\r\n"); $socket->write("Content-Type: application/x-www-form-urlencoded\r\n"); } foreach ($this->_headers as $header_line) { $socket->write($header_line . "\r\n"); } if (count($this->_cookies) > 0) { $socket->write("Cookie: " . $this->_marshallCookies($this->_cookies) . "\r\n"); } $socket->write("\r\n"); if ($encoding) { $socket->write($encoding->asString()); } } /** * Calculates the length of the encoded content. * @param SimpleFormEncoding $encoding Content to send with * request or false. */ function _getContentLength($encoding) { if (! $encoding) { return 0; } return (integer)strlen($encoding->asString()); } /** * Adds a header line to the request. * @param string $header_line Text of header line. * @access public */ function addHeaderLine($header_line) { $this->_headers[] = $header_line; } /** * Adds a cookie to the request. * @param SimpleCookie $cookie Additional cookie. * @access public */ function setCookie($cookie) { $this->_cookies[] = $cookie; } /** * Serialises the cookie hash ready for * transmission. * @param hash $cookies Parsed cookies. * @return array Cookies in header form. * @access private */ function _marshallCookies($cookies) { $cookie_pairs = array(); foreach ($cookies as $cookie) { $cookie_pairs[] = $cookie->getName() . "=" . $cookie->getValue(); } return implode(";", $cookie_pairs); } /** * Wraps the socket in a response parser. * @param SimpleSocket $socket Responding socket. * @return SimpleHttpResponse Parsed response object. * @access protected */ function _createResponse($socket) { return new SimpleHttpResponse( $socket, $this->_method, $this->_route->getUrl(), $this->_encoding); } } /** * Collection of header lines in the response. * @package SimpleTest * @subpackage WebTester */ class SimpleHttpHeaders { protected $_raw_headers; protected $_response_code; protected $_http_version; protected $_mime_type; protected $_location; protected $_cookies; protected $_authentication; protected $_realm; /** * Parses the incoming header block. * @param string $headers Header block. * @access public */ function SimpleHttpHeaders($headers) { $this->_raw_headers = $headers; $this->_response_code = false; $this->_http_version = false; $this->_mime_type = ''; $this->_location = false; $this->_cookies = array(); $this->_authentication = false; $this->_realm = false; foreach (split("\r\n", $headers) as $header_line) { $this->_parseHeaderLine($header_line); } } /** * Accessor for parsed HTTP protocol version. * @return integer HTTP error code. * @access public */ function getHttpVersion() { return $this->_http_version; } /** * Accessor for raw header block. * @return string All headers as raw string. * @access public */ function getRaw() { return $this->_raw_headers; } /** * Accessor for parsed HTTP error code. * @return integer HTTP error code. * @access public */ function getResponseCode() { return (integer)$this->_response_code; } /** * Returns the redirected URL or false if * no redirection. * @return string URL or false for none. * @access public */ function getLocation() { return $this->_location; } /** * Test to see if the response is a valid redirect. * @return boolean True if valid redirect. * @access public */ function isRedirect() { return in_array($this->_response_code, array(301, 302, 303, 307)) && (boolean)$this->getLocation(); } /** * Test to see if the response is an authentication * challenge. * @return boolean True if challenge. * @access public */ function isChallenge() { return ($this->_response_code == 401) && (boolean)$this->_authentication && (boolean)$this->_realm; } /** * Accessor for MIME type header information. * @return string MIME type. * @access public */ function getMimeType() { return $this->_mime_type; } /** * Accessor for authentication type. * @return string Type. * @access public */ function getAuthentication() { return $this->_authentication; } /** * Accessor for security realm. * @return string Realm. * @access public */ function getRealm() { return $this->_realm; } /** * Accessor for any new cookies. * @return array List of new cookies. * @access public */ function getNewCookies() { return $this->_cookies; } /** * Called on each header line to accumulate the held * data within the class. * @param string $header_line One line of header. * @access protected */ function _parseHeaderLine($header_line) { if (preg_match('/HTTP\/(\d+\.\d+)\s+(.*?)\s/i', $header_line, $matches)) { $this->_http_version = $matches[1]; $this->_response_code = $matches[2]; } if (preg_match('/Content-type:\s*(.*)/i', $header_line, $matches)) { $this->_mime_type = trim($matches[1]); } if (preg_match('/Location:\s*(.*)/i', $header_line, $matches)) { $this->_location = trim($matches[1]); } if (preg_match('/Set-cookie:(.*)/i', $header_line, $matches)) { $this->_cookies[] = $this->_parseCookie($matches[1]); } if (preg_match('/WWW-Authenticate:\s+(\S+)\s+realm=\"(.*?)\"/i', $header_line, $matches)) { $this->_authentication = $matches[1]; $this->_realm = trim($matches[2]); } } /** * Parse the Set-cookie content. * @param string $cookie_line Text after "Set-cookie:" * @return SimpleCookie New cookie object. * @access private */ function _parseCookie($cookie_line) { $parts = split(";", $cookie_line); $cookie = array(); preg_match('/\s*(.*?)\s*=(.*)/', array_shift($parts), $cookie); foreach ($parts as $part) { if (preg_match('/\s*(.*?)\s*=(.*)/', $part, $matches)) { $cookie[$matches[1]] = trim($matches[2]); } } return new SimpleCookie( $cookie[1], trim($cookie[2]), isset($cookie["path"]) ? $cookie["path"] : "", isset($cookie["expires"]) ? $cookie["expires"] : false); } } /** * Basic HTTP response. * @package SimpleTest * @subpackage WebTester */ class SimpleHttpResponse extends SimpleStickyError { protected $_method; protected $_url; protected $_request_data; protected $_sent; protected $_content; protected $_headers; /** * Constructor. Reads and parses the incoming * content and headers. * @param SimpleSocket $socket Network connection to fetch * response text from. * @param string $method HTTP request method. * @param SimpleUrl $url Resource name. * @param mixed $request_data Record of content sent. * @access public */ function SimpleHttpResponse($socket, $method, $url, $request_data = '') { $this->SimpleStickyError(); $this->_method = $method; $this->_url = $url; $this->_request_data = $request_data; $this->_sent = $socket->getSent(); $this->_content = false; $raw = $this->_readAll($socket); if ($socket->isError()) { $this->_setError('Error reading socket [' . $socket->getError() . ']'); return; } $this->_parse($raw); } /** * Splits up the headers and the rest of the content. * @param string $raw Content to parse. * @access private */ function _parse($raw) { if (! $raw) { $this->_setError('Nothing fetched'); $this->_headers = new SimpleHttpHeaders(''); } elseif (! strstr($raw, "\r\n\r\n")) { $this->_setError('Could not parse headers'); $this->_headers = new SimpleHttpHeaders($raw); } else { list($headers, $this->_content) = split("\r\n\r\n", $raw, 2); $this->_headers = new SimpleHttpHeaders($headers); } } /** * Original request method. * @return string GET, POST or HEAD. * @access public */ function getMethod() { return $this->_method; } /** * Resource name. * @return SimpleUrl Current url. * @access public */ function getUrl() { return $this->_url; } /** * Original request data. * @return mixed Sent content. * @access public */ function getRequestData() { return $this->_request_data; } /** * Raw request that was sent down the wire. * @return string Bytes actually sent. * @access public */ function getSent() { return $this->_sent; } /** * Accessor for the content after the last * header line. * @return string All content. * @access public */ function getContent() { return $this->_content; } /** * Accessor for header block. The response is the * combination of this and the content. * @return SimpleHeaders Wrapped header block. * @access public */ function getHeaders() { return $this->_headers; } /** * Accessor for any new cookies. * @return array List of new cookies. * @access public */ function getNewCookies() { return $this->_headers->getNewCookies(); } /** * Reads the whole of the socket output into a * single string. * @param SimpleSocket $socket Unread socket. * @return string Raw output if successful * else false. * @access private */ function _readAll($socket) { $all = ''; while (! $this->_isLastPacket($next = $socket->read())) { $all .= $next; } return $all; } /** * Test to see if the packet from the socket is the * last one. * @param string $packet Chunk to interpret. * @return boolean True if empty or EOF. * @access private */ function _isLastPacket($packet) { if (is_string($packet)) { return $packet === ''; } return ! $packet; } } ?>