1<?php 2/** 3 * base include file for SimpleTest 4 * @package SimpleTest 5 * @subpackage WebTester 6 * @version $Id$ 7 */ 8 9/**#@+ 10 * include other SimpleTest class files 11 */ 12require_once(dirname(__FILE__) . '/socket.php'); 13require_once(dirname(__FILE__) . '/cookies.php'); 14require_once(dirname(__FILE__) . '/url.php'); 15/**#@-*/ 16 17/** 18 * Creates HTTP headers for the end point of 19 * a HTTP request. 20 * @package SimpleTest 21 * @subpackage WebTester 22 */ 23class SimpleRoute { 24 private $url; 25 26 /** 27 * Sets the target URL. 28 * @param SimpleUrl $url URL as object. 29 * @access public 30 */ 31 function __construct($url) { 32 $this->url = $url; 33 } 34 35 /** 36 * Resource name. 37 * @return SimpleUrl Current url. 38 * @access protected 39 */ 40 function getUrl() { 41 return $this->url; 42 } 43 44 /** 45 * Creates the first line which is the actual request. 46 * @param string $method HTTP request method, usually GET. 47 * @return string Request line content. 48 * @access protected 49 */ 50 protected function getRequestLine($method) { 51 return $method . ' ' . $this->url->getPath() . 52 $this->url->getEncodedRequest() . ' HTTP/1.0'; 53 } 54 55 /** 56 * Creates the host part of the request. 57 * @return string Host line content. 58 * @access protected 59 */ 60 protected function getHostLine() { 61 $line = 'Host: ' . $this->url->getHost(); 62 if ($this->url->getPort()) { 63 $line .= ':' . $this->url->getPort(); 64 } 65 return $line; 66 } 67 68 /** 69 * Opens a socket to the route. 70 * @param string $method HTTP request method, usually GET. 71 * @param integer $timeout Connection timeout. 72 * @return SimpleSocket New socket. 73 * @access public 74 */ 75 function createConnection($method, $timeout) { 76 $default_port = ('https' == $this->url->getScheme()) ? 443 : 80; 77 $socket = $this->createSocket( 78 $this->url->getScheme() ? $this->url->getScheme() : 'http', 79 $this->url->getHost(), 80 $this->url->getPort() ? $this->url->getPort() : $default_port, 81 $timeout); 82 if (! $socket->isError()) { 83 $socket->write($this->getRequestLine($method) . "\r\n"); 84 $socket->write($this->getHostLine() . "\r\n"); 85 $socket->write("Connection: close\r\n"); 86 } 87 return $socket; 88 } 89 90 /** 91 * Factory for socket. 92 * @param string $scheme Protocol to use. 93 * @param string $host Hostname to connect to. 94 * @param integer $port Remote port. 95 * @param integer $timeout Connection timeout. 96 * @return SimpleSocket/SimpleSecureSocket New socket. 97 * @access protected 98 */ 99 protected function createSocket($scheme, $host, $port, $timeout) { 100 if (in_array($scheme, array('file'))) { 101 return new SimpleFileSocket($this->url); 102 } elseif (in_array($scheme, array('https'))) { 103 return new SimpleSecureSocket($host, $port, $timeout); 104 } else { 105 return new SimpleSocket($host, $port, $timeout); 106 } 107 } 108} 109 110/** 111 * Creates HTTP headers for the end point of 112 * a HTTP request via a proxy server. 113 * @package SimpleTest 114 * @subpackage WebTester 115 */ 116class SimpleProxyRoute extends SimpleRoute { 117 private $proxy; 118 private $username; 119 private $password; 120 121 /** 122 * Stashes the proxy address. 123 * @param SimpleUrl $url URL as object. 124 * @param string $proxy Proxy URL. 125 * @param string $username Username for autentication. 126 * @param string $password Password for autentication. 127 * @access public 128 */ 129 function __construct($url, $proxy, $username = false, $password = false) { 130 parent::__construct($url); 131 $this->proxy = $proxy; 132 $this->username = $username; 133 $this->password = $password; 134 } 135 136 /** 137 * Creates the first line which is the actual request. 138 * @param string $method HTTP request method, usually GET. 139 * @param SimpleUrl $url URL as object. 140 * @return string Request line content. 141 * @access protected 142 */ 143 function getRequestLine($method) { 144 $url = $this->getUrl(); 145 $scheme = $url->getScheme() ? $url->getScheme() : 'http'; 146 $port = $url->getPort() ? ':' . $url->getPort() : ''; 147 return $method . ' ' . $scheme . '://' . $url->getHost() . $port . 148 $url->getPath() . $url->getEncodedRequest() . ' HTTP/1.0'; 149 } 150 151 /** 152 * Creates the host part of the request. 153 * @param SimpleUrl $url URL as object. 154 * @return string Host line content. 155 * @access protected 156 */ 157 function getHostLine() { 158 $host = 'Host: ' . $this->proxy->getHost(); 159 $port = $this->proxy->getPort() ? $this->proxy->getPort() : 8080; 160 return "$host:$port"; 161 } 162 163 /** 164 * Opens a socket to the route. 165 * @param string $method HTTP request method, usually GET. 166 * @param integer $timeout Connection timeout. 167 * @return SimpleSocket New socket. 168 * @access public 169 */ 170 function createConnection($method, $timeout) { 171 $socket = $this->createSocket( 172 $this->proxy->getScheme() ? $this->proxy->getScheme() : 'http', 173 $this->proxy->getHost(), 174 $this->proxy->getPort() ? $this->proxy->getPort() : 8080, 175 $timeout); 176 if ($socket->isError()) { 177 return $socket; 178 } 179 $socket->write($this->getRequestLine($method) . "\r\n"); 180 $socket->write($this->getHostLine() . "\r\n"); 181 if ($this->username && $this->password) { 182 $socket->write('Proxy-Authorization: Basic ' . 183 base64_encode($this->username . ':' . $this->password) . 184 "\r\n"); 185 } 186 $socket->write("Connection: close\r\n"); 187 return $socket; 188 } 189} 190 191/** 192 * HTTP request for a web page. Factory for 193 * HttpResponse object. 194 * @package SimpleTest 195 * @subpackage WebTester 196 */ 197class SimpleHttpRequest { 198 private $route; 199 private $encoding; 200 private $headers; 201 private $cookies; 202 203 /** 204 * Builds the socket request from the different pieces. 205 * These include proxy information, URL, cookies, headers, 206 * request method and choice of encoding. 207 * @param SimpleRoute $route Request route. 208 * @param SimpleFormEncoding $encoding Content to send with 209 * request. 210 * @access public 211 */ 212 function __construct($route, $encoding) { 213 $this->route = $route; 214 $this->encoding = $encoding; 215 $this->headers = array(); 216 $this->cookies = array(); 217 } 218 219 /** 220 * Dispatches the content to the route's socket. 221 * @param integer $timeout Connection timeout. 222 * @return SimpleHttpResponse A response which may only have 223 * an error, but hopefully has a 224 * complete web page. 225 * @access public 226 */ 227 function fetch($timeout) { 228 $socket = $this->route->createConnection($this->encoding->getMethod(), $timeout); 229 if (! $socket->isError()) { 230 $this->dispatchRequest($socket, $this->encoding); 231 } 232 return $this->createResponse($socket); 233 } 234 235 /** 236 * Sends the headers. 237 * @param SimpleSocket $socket Open socket. 238 * @param string $method HTTP request method, 239 * usually GET. 240 * @param SimpleFormEncoding $encoding Content to send with request. 241 * @access private 242 */ 243 protected function dispatchRequest($socket, $encoding) { 244 foreach ($this->headers as $header_line) { 245 $socket->write($header_line . "\r\n"); 246 } 247 if (count($this->cookies) > 0) { 248 $socket->write("Cookie: " . implode(";", $this->cookies) . "\r\n"); 249 } 250 $encoding->writeHeadersTo($socket); 251 $socket->write("\r\n"); 252 $encoding->writeTo($socket); 253 } 254 255 /** 256 * Adds a header line to the request. 257 * @param string $header_line Text of full header line. 258 * @access public 259 */ 260 function addHeaderLine($header_line) { 261 $this->headers[] = $header_line; 262 } 263 264 /** 265 * Reads all the relevant cookies from the 266 * cookie jar. 267 * @param SimpleCookieJar $jar Jar to read 268 * @param SimpleUrl $url Url to use for scope. 269 * @access public 270 */ 271 function readCookiesFromJar($jar, $url) { 272 $this->cookies = $jar->selectAsPairs($url); 273 } 274 275 /** 276 * Wraps the socket in a response parser. 277 * @param SimpleSocket $socket Responding socket. 278 * @return SimpleHttpResponse Parsed response object. 279 * @access protected 280 */ 281 protected function createResponse($socket) { 282 $response = new SimpleHttpResponse( 283 $socket, 284 $this->route->getUrl(), 285 $this->encoding); 286 $socket->close(); 287 return $response; 288 } 289} 290 291/** 292 * Collection of header lines in the response. 293 * @package SimpleTest 294 * @subpackage WebTester 295 */ 296class SimpleHttpHeaders { 297 private $raw_headers; 298 private $response_code; 299 private $http_version; 300 private $mime_type; 301 private $location; 302 private $cookies; 303 private $authentication; 304 private $realm; 305 306 /** 307 * Parses the incoming header block. 308 * @param string $headers Header block. 309 * @access public 310 */ 311 function __construct($headers) { 312 $this->raw_headers = $headers; 313 $this->response_code = false; 314 $this->http_version = false; 315 $this->mime_type = ''; 316 $this->location = false; 317 $this->cookies = array(); 318 $this->authentication = false; 319 $this->realm = false; 320 foreach (explode("\r\n", $headers) as $header_line) { 321 $this->parseHeaderLine($header_line); 322 } 323 } 324 325 /** 326 * Accessor for parsed HTTP protocol version. 327 * @return integer HTTP error code. 328 * @access public 329 */ 330 function getHttpVersion() { 331 return $this->http_version; 332 } 333 334 /** 335 * Accessor for raw header block. 336 * @return string All headers as raw string. 337 * @access public 338 */ 339 function getRaw() { 340 return $this->raw_headers; 341 } 342 343 /** 344 * Accessor for parsed HTTP error code. 345 * @return integer HTTP error code. 346 * @access public 347 */ 348 function getResponseCode() { 349 return (integer)$this->response_code; 350 } 351 352 /** 353 * Returns the redirected URL or false if 354 * no redirection. 355 * @return string URL or false for none. 356 * @access public 357 */ 358 function getLocation() { 359 return $this->location; 360 } 361 362 /** 363 * Test to see if the response is a valid redirect. 364 * @return boolean True if valid redirect. 365 * @access public 366 */ 367 function isRedirect() { 368 return in_array($this->response_code, array(301, 302, 303, 307)) && 369 (boolean)$this->getLocation(); 370 } 371 372 /** 373 * Test to see if the response is an authentication 374 * challenge. 375 * @return boolean True if challenge. 376 * @access public 377 */ 378 function isChallenge() { 379 return ($this->response_code == 401) && 380 (boolean)$this->authentication && 381 (boolean)$this->realm; 382 } 383 384 /** 385 * Accessor for MIME type header information. 386 * @return string MIME type. 387 * @access public 388 */ 389 function getMimeType() { 390 return $this->mime_type; 391 } 392 393 /** 394 * Accessor for authentication type. 395 * @return string Type. 396 * @access public 397 */ 398 function getAuthentication() { 399 return $this->authentication; 400 } 401 402 /** 403 * Accessor for security realm. 404 * @return string Realm. 405 * @access public 406 */ 407 function getRealm() { 408 return $this->realm; 409 } 410 411 /** 412 * Writes new cookies to the cookie jar. 413 * @param SimpleCookieJar $jar Jar to write to. 414 * @param SimpleUrl $url Host and path to write under. 415 * @access public 416 */ 417 function writeCookiesToJar($jar, $url) { 418 foreach ($this->cookies as $cookie) { 419 $jar->setCookie( 420 $cookie->getName(), 421 $cookie->getValue(), 422 $url->getHost(), 423 $cookie->getPath(), 424 $cookie->getExpiry()); 425 } 426 } 427 428 /** 429 * Called on each header line to accumulate the held 430 * data within the class. 431 * @param string $header_line One line of header. 432 * @access protected 433 */ 434 protected function parseHeaderLine($header_line) { 435 if (preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)/i', $header_line, $matches)) { 436 $this->http_version = $matches[1]; 437 $this->response_code = $matches[2]; 438 } 439 if (preg_match('/Content-type:\s*(.*)/i', $header_line, $matches)) { 440 $this->mime_type = trim($matches[1]); 441 } 442 if (preg_match('/Location:\s*(.*)/i', $header_line, $matches)) { 443 $this->location = trim($matches[1]); 444 } 445 if (preg_match('/Set-cookie:(.*)/i', $header_line, $matches)) { 446 $this->cookies[] = $this->parseCookie($matches[1]); 447 } 448 if (preg_match('/WWW-Authenticate:\s+(\S+)\s+realm=\"(.*?)\"/i', $header_line, $matches)) { 449 $this->authentication = $matches[1]; 450 $this->realm = trim($matches[2]); 451 } 452 } 453 454 /** 455 * Parse the Set-cookie content. 456 * @param string $cookie_line Text after "Set-cookie:" 457 * @return SimpleCookie New cookie object. 458 * @access private 459 */ 460 protected function parseCookie($cookie_line) { 461 $parts = explode(";", $cookie_line); 462 $cookie = array(); 463 preg_match('/\s*(.*?)\s*=(.*)/', array_shift($parts), $cookie); 464 foreach ($parts as $part) { 465 if (preg_match('/\s*(.*?)\s*=(.*)/', $part, $matches)) { 466 $cookie[$matches[1]] = trim($matches[2]); 467 } 468 } 469 return new SimpleCookie( 470 $cookie[1], 471 trim($cookie[2]), 472 isset($cookie["path"]) ? $cookie["path"] : "", 473 isset($cookie["expires"]) ? $cookie["expires"] : false); 474 } 475} 476 477/** 478 * Basic HTTP response. 479 * @package SimpleTest 480 * @subpackage WebTester 481 */ 482class SimpleHttpResponse extends SimpleStickyError { 483 private $url; 484 private $encoding; 485 private $sent; 486 private $content; 487 private $headers; 488 489 /** 490 * Constructor. Reads and parses the incoming 491 * content and headers. 492 * @param SimpleSocket $socket Network connection to fetch 493 * response text from. 494 * @param SimpleUrl $url Resource name. 495 * @param mixed $encoding Record of content sent. 496 * @access public 497 */ 498 function __construct($socket, $url, $encoding) { 499 parent::__construct(); 500 $this->url = $url; 501 $this->encoding = $encoding; 502 $this->sent = $socket->getSent(); 503 $this->content = false; 504 $raw = $this->readAll($socket); 505 if ($socket->isError()) { 506 $this->setError('Error reading socket [' . $socket->getError() . ']'); 507 return; 508 } 509 $this->parse($raw); 510 } 511 512 /** 513 * Splits up the headers and the rest of the content. 514 * @param string $raw Content to parse. 515 * @access private 516 */ 517 protected function parse($raw) { 518 if (! $raw) { 519 $this->setError('Nothing fetched'); 520 $this->headers = new SimpleHttpHeaders(''); 521 } elseif ('file' == $this->url->getScheme()) { 522 $this->headers = new SimpleHttpHeaders(''); 523 $this->content = $raw; 524 } elseif (! strstr($raw, "\r\n\r\n")) { 525 $this->setError('Could not split headers from content'); 526 $this->headers = new SimpleHttpHeaders($raw); 527 } else { 528 list($headers, $this->content) = explode("\r\n\r\n", $raw, 2); 529 $this->headers = new SimpleHttpHeaders($headers); 530 } 531 } 532 533 /** 534 * Original request method. 535 * @return string GET, POST or HEAD. 536 * @access public 537 */ 538 function getMethod() { 539 return $this->encoding->getMethod(); 540 } 541 542 /** 543 * Resource name. 544 * @return SimpleUrl Current url. 545 * @access public 546 */ 547 function getUrl() { 548 return $this->url; 549 } 550 551 /** 552 * Original request data. 553 * @return mixed Sent content. 554 * @access public 555 */ 556 function getRequestData() { 557 return $this->encoding; 558 } 559 560 /** 561 * Raw request that was sent down the wire. 562 * @return string Bytes actually sent. 563 * @access public 564 */ 565 function getSent() { 566 return $this->sent; 567 } 568 569 /** 570 * Accessor for the content after the last 571 * header line. 572 * @return string All content. 573 * @access public 574 */ 575 function getContent() { 576 return $this->content; 577 } 578 579 /** 580 * Accessor for header block. The response is the 581 * combination of this and the content. 582 * @return SimpleHeaders Wrapped header block. 583 * @access public 584 */ 585 function getHeaders() { 586 return $this->headers; 587 } 588 589 /** 590 * Accessor for any new cookies. 591 * @return array List of new cookies. 592 * @access public 593 */ 594 function getNewCookies() { 595 return $this->headers->getNewCookies(); 596 } 597 598 /** 599 * Reads the whole of the socket output into a 600 * single string. 601 * @param SimpleSocket $socket Unread socket. 602 * @return string Raw output if successful 603 * else false. 604 * @access private 605 */ 606 protected function readAll($socket) { 607 $all = ''; 608 while (! $this->isLastPacket($next = $socket->read())) { 609 $all .= $next; 610 } 611 return $all; 612 } 613 614 /** 615 * Test to see if the packet from the socket is the 616 * last one. 617 * @param string $packet Chunk to interpret. 618 * @return boolean True if empty or EOF. 619 * @access private 620 */ 621 protected function isLastPacket($packet) { 622 if (is_string($packet)) { 623 return $packet === ''; 624 } 625 return ! $packet; 626 } 627} 628?>