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?>