summaryrefslogtreecommitdiff
path: root/tests/test_tools/simpletest/http.php
diff options
context:
space:
mode:
authorxue <>2006-07-07 14:54:15 +0000
committerxue <>2006-07-07 14:54:15 +0000
commit61bb16ee2e5f0a66234e1575242169a10fde47b5 (patch)
tree3ee24dcc36ceae2c213130df1ea3d5c9fc110a27 /tests/test_tools/simpletest/http.php
parent7b84938b1b5964f2274d66e28ba17435924ffe35 (diff)
Merge from 3.0 branch till 1253.
Diffstat (limited to 'tests/test_tools/simpletest/http.php')
-rw-r--r--tests/test_tools/simpletest/http.php853
1 files changed, 853 insertions, 0 deletions
diff --git a/tests/test_tools/simpletest/http.php b/tests/test_tools/simpletest/http.php
new file mode 100644
index 00000000..fa27de9c
--- /dev/null
+++ b/tests/test_tools/simpletest/http.php
@@ -0,0 +1,853 @@
+<?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;
+ }
+ }
+?> \ No newline at end of file