3 * base include file for SimpleTest
5 * @subpackage WebTester
6 * @version $Id: http.php,v 1.94 2004/06/30 22:13:07 lastcraft Exp $
10 * include other SimpleTest class files
12 require_once(dirname(__FILE__
) . '/socket.php');
13 require_once(dirname(__FILE__
) . '/url.php');
17 * Cookie data holder. Cookie rules are full of pretty
18 * arbitary stuff. I have used...
19 * http://wp.netscape.com/newsref/std/cookie_spec.html
20 * http://www.cookiecentral.com/faq/
22 * @subpackage WebTester
33 * Constructor. Sets the stored values.
34 * @param string $name Cookie key.
35 * @param string $value Value of cookie.
36 * @param string $path Cookie path if not host wide.
37 * @param string $expiry Expiry date as string.
38 * @param boolean $is_secure Currently ignored.
40 function SimpleCookie($name, $value = false
, $path = false
, $expiry = false
, $is_secure = false
) {
43 $this->_value
= $value;
44 $this->_path
= ($path ?
$this->_fixPath($path) : "/");
45 $this->_expiry
= false
;
46 if (is_string($expiry)) {
47 $this->_expiry
= strtotime($expiry);
48 } elseif (is_integer($expiry)) {
49 $this->_expiry
= $expiry;
51 $this->_is_secure
= $is_secure;
55 * Sets the host. The cookie rules determine
56 * that the first two parts are taken for
57 * certain TLDs and three for others. If the
58 * new host does not match these rules then the
60 * @param string $host New hostname.
61 * @return boolean True if hostname is valid.
64 function setHost($host) {
65 if ($host = $this->_truncateHost($host)) {
73 * Accessor for the truncated host to which this
75 * @return string Truncated hostname.
83 * Test for a cookie being valid for a host name.
84 * @param string $host Host to test against.
85 * @return boolean True if the cookie would be valid
88 function isValidHost($host) {
89 return ($this->_truncateHost($host) === $this->getHost());
93 * Extracts just the domain part that determines a
94 * cookie's host validity.
95 * @param string $host Host name to truncate.
96 * @return string Domain or false on a bad host.
99 function _truncateHost($host) {
100 if (preg_match('/[a-z\-]+\.(com|edu|net|org|gov|mil|int)$/i', $host, $matches)) {
102 } elseif (preg_match('/[a-z\-]+\.[a-z\-]+\.[a-z\-]+$/i', $host, $matches)) {
110 * @return string Cookie key.
118 * Accessor for value. A deleted cookie will
119 * have an empty string for this.
120 * @return string Cookie value.
123 function getValue() {
124 return $this->_value
;
129 * @return string Valid cookie path.
137 * Tests a path to see if the cookie applies
138 * there. The test path must be longer or
139 * equal to the cookie path.
140 * @param string $path Path to test against.
141 * @return boolean True if cookie valid here.
144 function isValidPath($path) {
146 $this->_fixPath($path),
148 strlen($this->getPath())) == 0);
152 * Accessor for expiry.
153 * @return string Expiry string.
156 function getExpiry() {
157 if (! $this->_expiry
) {
160 return gmdate("D, d M Y H:i:s", $this->_expiry
) . " GMT";
164 * Test to see if cookie is expired against
165 * the cookie format time or timestamp.
166 * Will give true for a session cookie.
167 * @param integer/string $now Time to test against. Result
168 * will be false if this time
169 * is later than the cookie expiry.
170 * Can be either a timestamp integer
171 * or a cookie format date.
174 function isExpired($now) {
175 if (! $this->_expiry
) {
178 if (is_string($now)) {
179 $now = strtotime($now);
181 return ($this->_expiry
< $now);
185 * Ages the cookie by the specified number of
187 * @param integer $interval In seconds.
190 function agePrematurely($interval) {
191 if ($this->_expiry
) {
192 $this->_expiry
-= $interval;
197 * Accessor for the secure flag.
198 * @return boolean True if cookie needs SSL.
201 function isSecure() {
202 return $this->_is_secure
;
206 * Adds a trailing and leading slash to the path
208 * @param string $path Path to fix.
211 function _fixPath($path) {
212 if (substr($path, 0, 1) != '/') {
215 if (substr($path, -1, 1) != '/') {
223 * Creates HTTP headers for the end point of
225 * @package SimpleTest
226 * @subpackage WebTester
232 * Sets the target URL.
233 * @param SimpleUrl $url URL as object.
236 function SimpleRoute($url) {
242 * @return SimpleUrl Current url.
250 * Creates the first line which is the actual request.
251 * @param string $method HTTP request method, usually GET.
252 * @return string Request line content.
255 function _getRequestLine($method) {
256 return $method . ' ' . $this->_url
->getPath() .
257 $this->_url
->getEncodedRequest() . ' HTTP/1.0';
261 * Creates the host part of the request.
262 * @return string Host line content.
265 function _getHostLine() {
266 $line = 'Host: ' . $this->_url
->getHost();
267 if ($this->_url
->getPort()) {
268 $line .= ':' . $this->_url
->getPort();
274 * Opens a socket to the route.
275 * @param string $method HTTP request method, usually GET.
276 * @param integer $timeout Connection timeout.
277 * @return SimpleSocket New socket.
280 function &createConnection($method, $timeout) {
281 $default_port = ('https' == $this->_url
->getScheme()) ?
443 : 80;
282 $socket = &$this->_createSocket(
283 $this->_url
->getScheme() ?
$this->_url
->getScheme() : 'http',
284 $this->_url
->getHost(),
285 $this->_url
->getPort() ?
$this->_url
->getPort() : $default_port,
287 if (! $socket->isError()) {
288 $socket->write($this->_getRequestLine($method) . "\r\n");
289 $socket->write($this->_getHostLine() . "\r\n");
290 $socket->write("Connection: close\r\n");
296 * Factory for socket.
297 * @param string $scheme Protocol to use.
298 * @param string $host Hostname to connect to.
299 * @param integer $port Remote port.
300 * @param integer $timeout Connection timeout.
301 * @return SimpleSocket/SimpleSecureSocket New socket.
304 function &_createSocket($scheme, $host, $port, $timeout) {
305 if (in_array($scheme, array('https'))) {
306 return new SimpleSecureSocket($host, $port, $timeout);
308 return new SimpleSocket($host, $port, $timeout);
313 * Creates HTTP headers for the end point of
314 * a HTTP request via a proxy server.
315 * @package SimpleTest
316 * @subpackage WebTester
318 class SimpleProxyRoute
extends SimpleRoute
{
324 * Stashes the proxy address.
325 * @param SimpleUrl $url URL as object.
326 * @param string $proxy Proxy URL.
327 * @param string $username Username for autentication.
328 * @param string $password Password for autentication.
331 function SimpleProxyRoute($url, $proxy, $username = false
, $password = false
) {
332 $this->SimpleRoute($url);
333 $this->_proxy
= $proxy;
334 $this->_username
= $username;
335 $this->_password
= $password;
339 * Creates the first line which is the actual request.
340 * @param string $method HTTP request method, usually GET.
341 * @param SimpleUrl $url URL as object.
342 * @return string Request line content.
345 function _getRequestLine($method) {
346 $url = $this->getUrl();
347 $scheme = $url->getScheme() ?
$url->getScheme() : 'http';
348 $port = $url->getPort() ?
':' . $url->getPort() : '';
349 return $method . ' ' . $scheme . '://' . $url->getHost() . $port .
350 $url->getPath() . $url->getEncodedRequest() . ' HTTP/1.0';
354 * Creates the host part of the request.
355 * @param SimpleUrl $url URL as object.
356 * @return string Host line content.
359 function _getHostLine() {
360 $host = 'Host: ' . $this->_proxy
->getHost();
361 $port = $this->_proxy
->getPort() ?
$this->_proxy
->getPort() : 8080;
362 return "$host:$port";
366 * Opens a socket to the route.
367 * @param string $method HTTP request method, usually GET.
368 * @param integer $timeout Connection timeout.
369 * @return SimpleSocket New socket.
372 function &createConnection($method, $timeout) {
373 $socket = &$this->_createSocket(
374 $this->_proxy
->getScheme() ?
$this->_proxy
->getScheme() : 'http',
375 $this->_proxy
->getHost(),
376 $this->_proxy
->getPort() ?
$this->_proxy
->getPort() : 8080,
378 if (! $socket->isError()) {
379 $socket->write($this->_getRequestLine($method) . "\r\n");
380 $socket->write($this->_getHostLine() . "\r\n");
381 if ($this->_username
&& $this->_password
) {
382 $socket->write('Proxy-Authorization: Basic ' .
383 base64_encode($this->_username
. ':' . $this->_password
) .
386 $socket->write("Connection: close\r\n");
393 * HTTP request for a web page. Factory for
394 * HttpResponse object.
395 * @package SimpleTest
396 * @subpackage WebTester
398 class SimpleHttpRequest
{
406 * Saves the URL ready for fetching.
407 * @param SimpleRoute $route Request route.
408 * @param string $method HTTP request method,
410 * @param string $content Content to send with request.
413 function SimpleHttpRequest(&$route, $method, $content = '') {
414 $this->_route
= &$route;
415 $this->_method
= $method;
416 $this->_content
= $content;
417 $this->_headers
= array();
418 $this->_cookies
= array();
422 * Fetches the content and parses the headers.
423 * @param integer $timeout Connection timeout.
424 * @return SimpleHttpResponse A response which may only have
428 function &fetch($timeout) {
429 $socket = &$this->_route
->createConnection($this->_method
, $timeout);
430 if ($socket->isError()) {
431 return $this->_createResponse($socket);
433 $this->_dispatchRequest($socket, $this->_method
, $this->_content
);
434 return $this->_createResponse($socket);
439 * @param SimpleSocket $socket Open socket.
440 * @param string $method HTTP request method,
442 * @param string $content Content to send with request.
445 function _dispatchRequest(&$socket, $method, $content) {
447 $socket->write("Content-Length: " . strlen($content) . "\r\n");
448 $socket->write("Content-Type: application/x-www-form-urlencoded\r\n");
450 foreach ($this->_headers
as $header_line) {
451 $socket->write($header_line . "\r\n");
453 if (count($this->_cookies
) > 0) {
454 $socket->write("Cookie: " . $this->_marshallCookies($this->_cookies
) . "\r\n");
456 $socket->write("\r\n");
458 $socket->write($content);
463 * Adds a header line to the request.
464 * @param string $header_line Text of header line.
467 function addHeaderLine($header_line) {
468 $this->_headers
[] = $header_line;
472 * Adds a cookie to the request.
473 * @param SimpleCookie $cookie Additional cookie.
476 function setCookie($cookie) {
477 $this->_cookies
[] = $cookie;
481 * Serialises the cookie hash ready for
483 * @param hash $cookies Parsed cookies.
484 * @return array Cookies in header form.
487 function _marshallCookies($cookies) {
488 $cookie_pairs = array();
489 foreach ($cookies as $cookie) {
490 $cookie_pairs[] = $cookie->getName() . "=" . $cookie->getValue();
492 return implode(";", $cookie_pairs);
496 * Wraps the socket in a response parser.
497 * @param SimpleSocket $socket Responding socket.
498 * @return SimpleHttpResponse Parsed response object.
501 function &_createResponse(&$socket) {
502 return new SimpleHttpResponse(
505 $this->_route
->getUrl(),
511 * Request with data to send. Usually PUT or POST.
512 * @package SimpleTest
513 * @subpackage WebTester
515 class SimpleHttpPostRequest
extends SimpleHttpRequest
{
518 * Cretaes an HTML form request.
519 * @param SimpleRoute $route Request target.
520 * @param array $parameters Content to send.
523 function SimpleHttpPostRequest($route, $parameters) {
524 $this->SimpleHttpRequest($route, 'POST', $parameters);
529 * @param SimpleSocket $socket Open socket.
530 * @param string $method HTTP request method,
532 * @param string $content Content to send with request.
535 function _dispatchRequest(&$socket, $method, $content) {
536 parent
::_dispatchRequest(
539 SimpleUrl
::encodeRequest($content));
544 * Collection of header lines in the response.
545 * @package SimpleTest
546 * @subpackage WebTester
548 class SimpleHttpHeaders
{
555 var $_authentication;
559 * Parses the incoming header block.
560 * @param string $headers Header block.
563 function SimpleHttpHeaders($headers) {
564 $this->_raw_headers
= $headers;
565 $this->_response_code
= false
;
566 $this->_http_version
= false
;
567 $this->_mime_type
= '';
568 $this->_location
= false
;
569 $this->_cookies
= array();
570 $this->_authentication
= false
;
571 $this->_realm
= false
;
572 foreach (explode("\r\n", $headers) as $header_line) {
573 $this->_parseHeaderLine($header_line);
578 * Accessor for parsed HTTP protocol version.
579 * @return integer HTTP error code.
582 function getHttpVersion() {
583 return $this->_http_version
;
587 * Accessor for raw header block.
588 * @return string All headers as raw string.
592 return $this->_raw_headers
;
596 * Accessor for parsed HTTP error code.
597 * @return integer HTTP error code.
600 function getResponseCode() {
601 return (integer)$this->_response_code
;
605 * Returns the redirected URL or false if
607 * @return string URL or false for none.
610 function getLocation() {
611 return $this->_location
;
615 * Test to see if the response is a valid redirect.
616 * @return boolean True if valid redirect.
619 function isRedirect() {
620 return in_array($this->_response_code
, array(301, 302, 303, 307)) &&
621 (boolean
)$this->getLocation();
625 * Test to see if the response is an authentication
627 * @return boolean True if challenge.
630 function isChallenge() {
631 return ($this->_response_code
== 401) &&
632 (boolean
)$this->_authentication
&&
633 (boolean
)$this->_realm
;
637 * Accessor for MIME type header information.
638 * @return string MIME type.
641 function getMimeType() {
642 return $this->_mime_type
;
646 * Accessor for authentication type.
647 * @return string Type.
650 function getAuthentication() {
651 return $this->_authentication
;
655 * Accessor for security realm.
656 * @return string Realm.
659 function getRealm() {
660 return $this->_realm
;
664 * Accessor for any new cookies.
665 * @return array List of new cookies.
668 function getNewCookies() {
669 return $this->_cookies
;
673 * Called on each header line to accumulate the held
674 * data within the class.
675 * @param string $header_line One line of header.
678 function _parseHeaderLine($header_line) {
679 if (preg_match('/HTTP\/(\d+\.\d+)\s+(.*?)\s/i', $header_line, $matches)) {
680 $this->_http_version
= $matches[1];
681 $this->_response_code
= $matches[2];
683 if (preg_match('/Content-type:\s*(.*)/i', $header_line, $matches)) {
684 $this->_mime_type
= trim($matches[1]);
686 if (preg_match('/Location:\s*(.*)/i', $header_line, $matches)) {
687 $this->_location
= trim($matches[1]);
689 if (preg_match('/Set-cookie:(.*)/i', $header_line, $matches)) {
690 $this->_cookies
[] = $this->_parseCookie($matches[1]);
692 if (preg_match('/WWW-Authenticate:\s+(\S+)\s+realm=\"(.*?)\"/i', $header_line, $matches)) {
693 $this->_authentication
= $matches[1];
694 $this->_realm
= trim($matches[2]);
699 * Parse the Set-cookie content.
700 * @param string $cookie_line Text after "Set-cookie:"
701 * @return SimpleCookie New cookie object.
704 function _parseCookie($cookie_line) {
705 $parts = explode(";", $cookie_line);
707 preg_match('/\s*(.*?)\s*=(.*)/', array_shift($parts), $cookie);
708 foreach ($parts as $part) {
709 if (preg_match('/\s*(.*?)\s*=(.*)/', $part, $matches)) {
710 $cookie[$matches[1]] = trim($matches[2]);
713 return new SimpleCookie(
716 isset($cookie["path"]) ?
$cookie["path"] : "",
717 isset($cookie["expires"]) ?
$cookie["expires"] : false
);
722 * Basic HTTP response.
723 * @package SimpleTest
724 * @subpackage WebTester
726 class SimpleHttpResponse
extends StickyError
{
735 * Constructor. Reads and parses the incoming
736 * content and headers.
737 * @param SimpleSocket $socket Network connection to fetch
738 * response text from.
739 * @param string $method HTTP request method.
740 * @param SimpleUrl $url Resource name.
741 * @param mixed $request_data Record of content sent.
744 function SimpleHttpResponse(&$socket, $method, $url, $request_data = '') {
745 $this->StickyError();
746 $this->_method
= $method;
748 $this->_request_data
= $request_data;
749 $this->_sent
= $socket->getSent();
750 $this->_content
= false
;
751 $raw = $this->_readAll($socket);
752 if ($socket->isError()) {
753 $this->_setError('Error reading socket [' . $socket->getError() . ']');
760 * Splits up the headers and the rest of the content.
761 * @param string $raw Content to parse.
764 function _parse($raw) {
766 $this->_setError('Nothing fetched');
767 $this->_headers
= &new SimpleHttpHeaders('');
768 } elseif (! strstr($raw, "\r\n\r\n")) {
769 $this->_setError('Could not parse headers');
770 $this->_headers
= &new SimpleHttpHeaders($raw);
772 list($headers, $this->_content
) = explode("\r\n\r\n", $raw, 2);
773 $this->_headers
= &new SimpleHttpHeaders($headers);
778 * Original request method.
779 * @return string GET, POST or HEAD.
782 function getMethod() {
783 return $this->_method
;
788 * @return SimpleUrl Current url.
796 * Original request data.
797 * @return mixed Sent content.
800 function getRequestData() {
801 return $this->_request_data
;
805 * Raw request that was sent down the wire.
806 * @return string Bytes actually sent.
814 * Accessor for the content after the last
816 * @return string All content.
819 function getContent() {
820 return $this->_content
;
824 * Accessor for header block. The response is the
825 * combination of this and the content.
826 * @return SimpleHeaders Wrapped header block.
829 function getHeaders() {
830 return $this->_headers
;
834 * Accessor for any new cookies.
835 * @return array List of new cookies.
838 function getNewCookies() {
839 return $this->_headers
->getNewCookies();
843 * Reads the whole of the socket output into a
845 * @param SimpleSocket $socket Unread socket.
846 * @return string Raw output if successful
850 function _readAll(&$socket) {
852 while (! $this->_isLastPacket($next = $socket->read())) {
859 * Test to see if the packet from the socket is the
861 * @param string $packet Chunk to interpret.
862 * @return boolean True if empty or EOF.
865 function _isLastPacket($packet) {
866 if (is_string($packet)) {
867 return $packet === '';