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