1<?php
2if (!defined('DOKU_INC'))
3    define('DOKU_INC', realpath(dirname(__FILE__) . '/../../../') . '/');
4
5require_once (DOKU_INC . 'inc/HTTPClient.php');
6
7define('HTTP_NL', "\r\n");
8
9/**
10 * Modifies sendRequest. If max_bodysize_limit is set to true, the size of
11 * the retrieved body is limited to the value set in max_bodysize.
12 *
13 * Also, modifies get and post to allow response codes in the 200 range.
14 *
15 * @author Gina Haeussge <osd@foosel.net>
16 */
17class LinkbackHTTPClient extends DokuHTTPClient {
18
19    var $max_bodysize_limit = false;
20
21    function __construct() {
22        parent::__construct();
23    }
24
25    /**
26     * Simple function to do a GET request
27     *
28     * Returns the wanted page or false on an error;
29     *
30     * @param  string $url       The URL to fetch
31     * @param  bool   $sloppy304 Return body on 304 not modified
32     * @author Andreas Gohr <andi@splitbrain.org>
33     */
34    function get($url,$sloppy304=false){
35        if(!$this->sendRequest($url)) return false;
36        if($this->status == 304 && $sloppy304) return $this->resp_body;
37        if($this->status < 200 || $this->status > 206) return false;
38        return $this->resp_body;
39    }
40
41    /**
42     * Simple function to do a POST request
43     *
44     * Returns the resulting page or false on an error;
45     *
46     * @author Andreas Gohr <andi@splitbrain.org>
47     */
48    function post($url,$data){
49        if(!$this->sendRequest($url,$data,'POST')) return false;
50        if($this->status < 200 || $this->status > 206) return false;
51        return $this->resp_body;
52    }
53
54    /**
55     * Do an HTTP request
56     *
57     * @author Andreas Goetz <cpuidle@gmx.de>
58     * @author Andreas Gohr <andi@splitbrain.org>
59     */
60    function sendRequest($url, $data = array (), $method = 'GET') {
61        $this->error = '';
62        $this->status = 0;
63
64        // parse URL into bits
65        $uri = parse_url($url);
66        $server = $uri['host'];
67        $path = $uri['path'];
68        if (empty ($path))
69            $path = '/';
70        if (!empty ($uri['query']))
71            $path .= '?' . $uri['query'];
72        $port = $uri['port'];
73        if ($uri['user'])
74            $this->user = $uri['user'];
75        if ($uri['pass'])
76            $this->pass = $uri['pass'];
77
78        // proxy setup
79        if ($this->proxy_host) {
80            $request_url = $url;
81            $server = $this->proxy_host;
82            $port = $this->proxy_port;
83            if (empty ($port))
84                $port = 8080;
85        } else {
86            $request_url = $path;
87            $server = $server;
88            if (empty ($port))
89                $port = ($uri['scheme'] == 'https') ? 443 : 80;
90        }
91
92        // add SSL stream prefix if needed - needs SSL support in PHP
93        if ($port == 443 || $this->proxy_ssl)
94            $server = 'ssl://' . $server;
95
96        // prepare headers
97        $headers = $this->headers;
98        $headers['Host'] = $uri['host'];
99        $headers['User-Agent'] = $this->agent;
100        $headers['Referer'] = $this->referer;
101        $headers['Connection'] = 'Close';
102        if ($method == 'POST') {
103            $post = $this->_postEncode($data);
104            $headers['Content-Type'] = 'application/x-www-form-urlencoded';
105            $headers['Content-Length'] = strlen($post);
106        }
107        if ($this->user) {
108            $headers['Authorization'] = 'BASIC ' . base64_encode($this->user . ':' . $this->pass);
109        }
110        if ($this->proxy_user) {
111            $headers['Proxy-Authorization'] = 'BASIC ' . base64_encode($this->proxy_user . ':' . $this->proxy_pass);
112        }
113
114        // stop time
115        $start = time();
116
117        // open socket
118        $socket = @ fsockopen($server, $port, $errno, $errstr, $this->timeout);
119        if (!$socket) {
120            $resp->status = '-100';
121            $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
122            return false;
123        }
124        //set non blocking
125        stream_set_blocking($socket, 0);
126
127        // build request
128        $request = "$method $request_url HTTP/" . $this->http . HTTP_NL;
129        $request .= $this->_buildHeaders($headers);
130        $request .= $this->_getCookies();
131        $request .= HTTP_NL;
132        $request .= $post;
133
134        $this->_debug('request', $request);
135
136        // send request
137        fputs($socket, $request);
138        // read headers from socket
139        $r_headers = '';
140        do {
141            if (time() - $start > $this->timeout) {
142                $this->status = -100;
143                $this->error = 'Timeout while reading headers';
144                return false;
145            }
146            if (feof($socket)) {
147                $this->error = 'Premature End of File (socket)';
148                return false;
149            }
150            $r_headers .= fread($socket, 1); #FIXME read full lines here?
151        } while (!preg_match('/\r\n\r\n$/', $r_headers));
152
153        $this->_debug('response headers', $r_headers);
154
155        // check if expected body size exceeds allowance
156        if ($this->max_bodysize && preg_match('/\r\nContent-Length:\s*(\d+)\r\n/i', $r_headers, $match)) {
157            if ($match[1] > $this->max_bodysize) {
158                $this->error = 'Reported content length exceeds allowed response size';
159                if (!$this->max_bodysize_limit)
160                    return false;
161            }
162        }
163
164        // get Status
165        if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/', $r_headers, $m)) {
166            $this->error = 'Server returned bad answer';
167            return false;
168        }
169        $this->status = $m[2];
170
171        // handle headers and cookies
172        $this->resp_headers = $this->_parseHeaders($r_headers);
173        if (isset ($this->resp_headers['set-cookie'])) {
174            foreach ((array) $this->resp_headers['set-cookie'] as $c) {
175                list ($key, $value, $foo) = explode('=', $cookie);
176                $this->cookies[$key] = $value;
177            }
178        }
179
180        $this->_debug('Object headers', $this->resp_headers);
181
182        // check server status code to follow redirect
183        if ($this->status == 301 || $this->status == 302) {
184            if (empty ($this->resp_headers['location'])) {
185                $this->error = 'Redirect but no Location Header found';
186                return false;
187            }
188            elseif ($this->redirect_count == $this->max_redirect) {
189                $this->error = 'Maximum number of redirects exceeded';
190                return false;
191            } else {
192                $this->redirect_count++;
193                $this->referer = $url;
194                if (!preg_match('/^http/i', $this->resp_headers['location'])) {
195                    $this->resp_headers['location'] = $uri['scheme'] . '://' . $uri['host'] .
196                    $this->resp_headers['location'];
197                }
198                // perform redirected request, always via GET (required by RFC)
199                return $this->sendRequest($this->resp_headers['location'], array (), 'GET');
200            }
201        }
202
203        // check if headers are as expected
204        if ($this->header_regexp && !preg_match($this->header_regexp, $r_headers)) {
205            $this->error = 'The received headers did not match the given regexp';
206            return false;
207        }
208
209        //read body (with chunked encoding if needed)
210        $r_body = '';
211        if (preg_match('/transfer\-(en)?coding:\s*chunked\r\n/i', $r_header)) {
212            do {
213                unset ($chunk_size);
214                do {
215                    if (feof($socket)) {
216                        $this->error = 'Premature End of File (socket)';
217                        return false;
218                    }
219                    if (time() - $start > $this->timeout) {
220                        $this->status = -100;
221                        $this->error = 'Timeout while reading chunk';
222                        return false;
223                    }
224                    $byte = fread($socket, 1);
225                    $chunk_size .= $byte;
226                } while (preg_match('/[a-zA-Z0-9]/', $byte)); // read chunksize including \r
227
228                $byte = fread($socket, 1); // readtrailing \n
229                $chunk_size = hexdec($chunk_size);
230                $this_chunk = fread($socket, $chunk_size);
231                $r_body .= $this_chunk;
232                if ($chunk_size)
233                    $byte = fread($socket, 2); // read trailing \r\n
234
235                if ($this->max_bodysize && strlen($r_body) > $this->max_bodysize) {
236                    $this->error = 'Allowed response size exceeded';
237                    if ($this->max_bodysize_limit)
238                        break;
239                    else
240                        return false;
241                }
242            }
243            while ($chunk_size);
244        } else {
245            // read entire socket
246            while (!feof($socket)) {
247                if (time() - $start > $this->timeout) {
248                    $this->status = -100;
249                    $this->error = 'Timeout while reading response';
250                    return false;
251                }
252                $r_body .= fread($socket, 4096);
253                if ($this->max_bodysize && strlen($r_body) > $this->max_bodysize) {
254                    $this->error = 'Allowed response size exceeded';
255                    if ($this->max_bodysize_limit)
256                        break;
257                    else
258                        return false;
259                }
260            }
261        }
262
263        // close socket
264        $status = socket_get_status($socket);
265        fclose($socket);
266
267        // decode gzip if needed
268        if ($this->resp_headers['content-encoding'] == 'gzip') {
269            $this->resp_body = gzinflate(substr($r_body, 10));
270        } else {
271            $this->resp_body = $r_body;
272        }
273
274        $this->_debug('response body', $this->resp_body);
275        $this->redirect_count = 0;
276        return true;
277    }
278
279}
280