Completed
Pull Request — remoteapiGetversions (#1573)
by Gerrit
06:07 queued 01:35
created

HTTPClient::get()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 8.8571
cc 6
eloc 5
nc 4
nop 2
1
<?php
2
/**
3
 * HTTP Client
4
 *
5
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6
 * @author     Andreas Goetz <[email protected]>
7
 */
8
9
10
define('HTTP_NL',"\r\n");
11
12
13
/**
14
 * Adds DokuWiki specific configs to the HTTP client
15
 *
16
 * @author Andreas Goetz <[email protected]>
17
 */
18
class DokuHTTPClient extends HTTPClient {
19
20
    /**
21
     * Constructor.
22
     *
23
     * @author Andreas Gohr <[email protected]>
24
     */
25
    function __construct(){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
26
        global $conf;
27
28
        // call parent constructor
29
        parent::__construct();
30
31
        // set some values from the config
32
        $this->proxy_host   = $conf['proxy']['host'];
0 ignored issues
show
Bug introduced by
The property proxy_host cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
33
        $this->proxy_port   = $conf['proxy']['port'];
0 ignored issues
show
Bug introduced by
The property proxy_port cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
34
        $this->proxy_user   = $conf['proxy']['user'];
0 ignored issues
show
Bug introduced by
The property proxy_user cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
35
        $this->proxy_pass   = conf_decodeString($conf['proxy']['pass']);
0 ignored issues
show
Bug introduced by
The property proxy_pass cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
36
        $this->proxy_ssl    = $conf['proxy']['ssl'];
0 ignored issues
show
Bug introduced by
The property proxy_ssl cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
37
        $this->proxy_except = $conf['proxy']['except'];
0 ignored issues
show
Bug introduced by
The property proxy_except cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
38
39
        // allow enabling debugging via URL parameter (if debugging allowed)
40
        if($conf['allowdebug']) {
41
            if(
42
                isset($_REQUEST['httpdebug']) ||
43
                (
44
                    isset($_SERVER['HTTP_REFERER']) &&
45
                    strpos($_SERVER['HTTP_REFERER'], 'httpdebug') !== false
46
                )
47
            ) {
48
                $this->debug = true;
0 ignored issues
show
Bug introduced by
The property debug cannot be accessed from this context as it is declared private in class HTTPClient.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
49
            }
50
        }
51
    }
52
53
54
    /**
55
     * Wraps an event around the parent function
56
     *
57
     * @triggers HTTPCLIENT_REQUEST_SEND
58
     * @author   Andreas Gohr <[email protected]>
59
     */
60
    /**
61
     * @param string $url
62
     * @param string|array $data the post data either as array or raw data
63
     * @param string $method
64
     * @return bool
65
     */
66
    function sendRequest($url,$data='',$method='GET'){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
67
        $httpdata = array('url'    => $url,
68
                          'data'   => $data,
69
                          'method' => $method);
70
        $evt = new Doku_Event('HTTPCLIENT_REQUEST_SEND',$httpdata);
71
        if($evt->advise_before()){
72
            $url    = $httpdata['url'];
73
            $data   = $httpdata['data'];
74
            $method = $httpdata['method'];
75
        }
76
        $evt->advise_after();
77
        unset($evt);
78
        return parent::sendRequest($url,$data,$method);
79
    }
80
81
}
82
83
/**
84
 * Class HTTPClientException
85
 */
86
class HTTPClientException extends Exception { }
87
88
/**
89
 * This class implements a basic HTTP client
90
 *
91
 * It supports POST and GET, Proxy usage, basic authentication,
92
 * handles cookies and referers. It is based upon the httpclient
93
 * function from the VideoDB project.
94
 *
95
 * @link   http://www.splitbrain.org/go/videodb
96
 * @author Andreas Goetz <[email protected]>
97
 * @author Andreas Gohr <[email protected]>
98
 * @author Tobias Sarnowski <[email protected]>
99
 */
100
class HTTPClient {
101
    //set these if you like
102
    var $agent;         // User agent
103
    var $http;          // HTTP version defaults to 1.0
104
    var $timeout;       // read timeout (seconds)
105
    var $cookies;
106
    var $referer;
107
    var $max_redirect;
108
    var $max_bodysize;
109
    var $max_bodysize_abort = true;  // if set, abort if the response body is bigger than max_bodysize
110
    var $header_regexp; // if set this RE must match against the headers, else abort
111
    var $headers;
112
    var $debug;
113
    var $start = 0.0; // for timings
114
    var $keep_alive = true; // keep alive rocks
115
116
    // don't set these, read on error
117
    var $error;
118
    var $redirect_count;
119
120
    // read these after a successful request
121
    var $status;
122
    var $resp_body;
123
    var $resp_headers;
124
125
    // set these to do basic authentication
126
    var $user;
127
    var $pass;
128
129
    // set these if you need to use a proxy
130
    var $proxy_host;
131
    var $proxy_port;
132
    var $proxy_user;
133
    var $proxy_pass;
134
    var $proxy_ssl; //boolean set to true if your proxy needs SSL
135
    var $proxy_except; // regexp of URLs to exclude from proxy
136
137
    // list of kept alive connections
138
    static $connections = array();
139
140
    // what we use as boundary on multipart/form-data posts
141
    var $boundary = '---DokuWikiHTTPClient--4523452351';
142
143
    /**
144
     * Constructor.
145
     *
146
     * @author Andreas Gohr <[email protected]>
147
     */
148
    function __construct(){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
149
        $this->agent        = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; '.PHP_OS.')';
150
        $this->timeout      = 15;
151
        $this->cookies      = array();
152
        $this->referer      = '';
153
        $this->max_redirect = 3;
154
        $this->redirect_count = 0;
155
        $this->status       = 0;
156
        $this->headers      = array();
157
        $this->http         = '1.0';
158
        $this->debug        = false;
159
        $this->max_bodysize = 0;
160
        $this->header_regexp= '';
161
        if(extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
162
        $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,'.
163
                                   'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
164
        $this->headers['Accept-Language'] = 'en-us';
165
    }
166
167
168
    /**
169
     * Simple function to do a GET request
170
     *
171
     * Returns the wanted page or false on an error;
172
     *
173
     * @param  string $url       The URL to fetch
174
     * @param  bool   $sloppy304 Return body on 304 not modified
175
     * @return false|string  response body, false on error
176
     *
177
     * @author Andreas Gohr <[email protected]>
178
     */
179
    function get($url,$sloppy304=false){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
180
        if(!$this->sendRequest($url)) return false;
181
        if($this->status == 304 && $sloppy304) return $this->resp_body;
182
        if($this->status < 200 || $this->status > 206) return false;
183
        return $this->resp_body;
184
    }
185
186
    /**
187
     * Simple function to do a GET request with given parameters
188
     *
189
     * Returns the wanted page or false on an error.
190
     *
191
     * This is a convenience wrapper around get(). The given parameters
192
     * will be correctly encoded and added to the given base URL.
193
     *
194
     * @param  string $url       The URL to fetch
195
     * @param  array  $data      Associative array of parameters
196
     * @param  bool   $sloppy304 Return body on 304 not modified
197
     * @return false|string  response body, false on error
198
     *
199
     * @author Andreas Gohr <[email protected]>
200
     */
201
    function dget($url,$data,$sloppy304=false){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
202
        if(strpos($url,'?')){
203
            $url .= '&';
204
        }else{
205
            $url .= '?';
206
        }
207
        $url .= $this->_postEncode($data);
208
        return $this->get($url,$sloppy304);
209
    }
210
211
    /**
212
     * Simple function to do a POST request
213
     *
214
     * Returns the resulting page or false on an error;
215
     *
216
     * @param  string $url       The URL to fetch
217
     * @param  array  $data      Associative array of parameters
218
     * @return false|string  response body, false on error
219
     * @author Andreas Gohr <[email protected]>
220
     */
221
    function post($url,$data){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
222
        if(!$this->sendRequest($url,$data,'POST')) return false;
223
        if($this->status < 200 || $this->status > 206) return false;
224
        return $this->resp_body;
225
    }
226
227
    /**
228
     * Send an HTTP request
229
     *
230
     * This method handles the whole HTTP communication. It respects set proxy settings,
231
     * builds the request headers, follows redirects and parses the response.
232
     *
233
     * Post data should be passed as associative array. When passed as string it will be
234
     * sent as is. You will need to setup your own Content-Type header then.
235
     *
236
     * @param  string $url    - the complete URL
237
     * @param  mixed  $data   - the post data either as array or raw data
238
     * @param  string $method - HTTP Method usually GET or POST.
239
     * @return bool - true on success
240
     *
241
     * @author Andreas Goetz <[email protected]>
242
     * @author Andreas Gohr <[email protected]>
243
     */
244
    function sendRequest($url,$data='',$method='GET'){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
245
        $this->start  = $this->_time();
246
        $this->error  = '';
247
        $this->status = 0;
248
        $this->status = 0;
249
        $this->resp_body = '';
250
        $this->resp_headers = array();
251
252
        // don't accept gzip if truncated bodies might occur
253
        if($this->max_bodysize &&
254
           !$this->max_bodysize_abort &&
255
           $this->headers['Accept-encoding'] == 'gzip'){
256
            unset($this->headers['Accept-encoding']);
257
        }
258
259
        // parse URL into bits
260
        $uri = parse_url($url);
261
        $server = $uri['host'];
262
        $path   = $uri['path'];
263
        if(empty($path)) $path = '/';
264
        if(!empty($uri['query'])) $path .= '?'.$uri['query'];
265
        if(!empty($uri['port'])) $port = $uri['port'];
266
        if(isset($uri['user'])) $this->user = $uri['user'];
267
        if(isset($uri['pass'])) $this->pass = $uri['pass'];
268
269
        // proxy setup
270
        if($this->proxy_host && (!$this->proxy_except || !preg_match('/'.$this->proxy_except.'/i',$url)) ){
271
            $request_url = $url;
272
            $server      = $this->proxy_host;
273
            $port        = $this->proxy_port;
274
            if (empty($port)) $port = 8080;
275
            $use_tls     = $this->proxy_ssl;
276
        }else{
277
            $request_url = $path;
278
            if (!isset($port)) $port = ($uri['scheme'] == 'https') ? 443 : 80;
279
            $use_tls     = ($uri['scheme'] == 'https');
280
        }
281
282
        // add SSL stream prefix if needed - needs SSL support in PHP
283
        if($use_tls) {
284
            if(!in_array('ssl', stream_get_transports())) {
285
                $this->status = -200;
286
                $this->error = 'This PHP version does not support SSL - cannot connect to server';
287
            }
288
            $server = 'ssl://'.$server;
289
        }
290
291
        // prepare headers
292
        $headers               = $this->headers;
293
        $headers['Host']       = $uri['host'];
294
        if(!empty($uri['port'])) $headers['Host'].= ':'.$uri['port'];
295
        $headers['User-Agent'] = $this->agent;
296
        $headers['Referer']    = $this->referer;
297
298
        if($method == 'POST'){
299
            if(is_array($data)){
300
                if($headers['Content-Type'] == 'multipart/form-data'){
301
                    $headers['Content-Type']   = 'multipart/form-data; boundary='.$this->boundary;
302
                    $data = $this->_postMultipartEncode($data);
303
                }else{
304
                    $headers['Content-Type']   = 'application/x-www-form-urlencoded';
305
                    $data = $this->_postEncode($data);
306
                }
307
            }
308
            $headers['Content-Length'] = strlen($data);
309
        }elseif($method == 'GET'){
310
            $data = ''; //no data allowed on GET requests
311
        }
312
        if($this->user) {
313
            $headers['Authorization'] = 'Basic '.base64_encode($this->user.':'.$this->pass);
314
        }
315
        if($this->proxy_user) {
316
            $headers['Proxy-Authorization'] = 'Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass);
317
        }
318
319
        // already connected?
320
        $connectionId = $this->_uniqueConnectionId($server,$port);
321
        $this->_debug('connection pool', self::$connections);
322
        $socket = null;
323
        if (isset(self::$connections[$connectionId])) {
324
            $this->_debug('reusing connection', $connectionId);
325
            $socket = self::$connections[$connectionId];
326
        }
327
        if (is_null($socket) || feof($socket)) {
328
            $this->_debug('opening connection', $connectionId);
329
            // open socket
330
            $socket = @fsockopen($server,$port,$errno, $errstr, $this->timeout);
331
            if (!$socket){
332
                $this->status = -100;
333
                $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
334
                return false;
335
            }
336
337
            // try establish a CONNECT tunnel for SSL
338
            try {
339
                if($this->_ssltunnel($socket, $request_url)){
340
                    // no keep alive for tunnels
341
                    $this->keep_alive = false;
342
                    // tunnel is authed already
343
                    if(isset($headers['Proxy-Authentication'])) unset($headers['Proxy-Authentication']);
344
                }
345
            } catch (HTTPClientException $e) {
346
                $this->status = $e->getCode();
347
                $this->error = $e->getMessage();
348
                fclose($socket);
349
                return false;
350
            }
351
352
            // keep alive?
353
            if ($this->keep_alive) {
354
                self::$connections[$connectionId] = $socket;
355
            } else {
356
                unset(self::$connections[$connectionId]);
357
            }
358
        }
359
360
        if ($this->keep_alive && !$this->proxy_host) {
361
            // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
362
            // connection token to a proxy server. We still do keep the connection the
363
            // proxy alive (well except for CONNECT tunnels)
364
            $headers['Connection'] = 'Keep-Alive';
365
        } else {
366
            $headers['Connection'] = 'Close';
367
        }
368
369
        try {
370
            //set non-blocking
371
            stream_set_blocking($socket, 0);
372
373
            // build request
374
            $request  = "$method $request_url HTTP/".$this->http.HTTP_NL;
375
            $request .= $this->_buildHeaders($headers);
376
            $request .= $this->_getCookies();
377
            $request .= HTTP_NL;
378
            $request .= $data;
379
380
            $this->_debug('request',$request);
381
            $this->_sendData($socket, $request, 'request');
382
383
            // read headers from socket
384
            $r_headers = '';
385
            do{
386
                $r_line = $this->_readLine($socket, 'headers');
387
                $r_headers .= $r_line;
388
            }while($r_line != "\r\n" && $r_line != "\n");
389
390
            $this->_debug('response headers',$r_headers);
391
392
            // check if expected body size exceeds allowance
393
            if($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i',$r_headers,$match)){
394
                if($match[1] > $this->max_bodysize){
395
                    if ($this->max_bodysize_abort)
396
                        throw new HTTPClientException('Reported content length exceeds allowed response size');
397
                    else
398
                        $this->error = 'Reported content length exceeds allowed response size';
399
                }
400
            }
401
402
            // get Status
403
            if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/', $r_headers, $m))
404
                throw new HTTPClientException('Server returned bad answer '.$r_headers);
405
406
            $this->status = $m[2];
407
408
            // handle headers and cookies
409
            $this->resp_headers = $this->_parseHeaders($r_headers);
410
            if(isset($this->resp_headers['set-cookie'])){
411
                foreach ((array) $this->resp_headers['set-cookie'] as $cookie){
412
                    list($cookie)   = explode(';',$cookie,2);
413
                    list($key,$val) = explode('=',$cookie,2);
414
                    $key = trim($key);
415
                    if($val == 'deleted'){
416
                        if(isset($this->cookies[$key])){
417
                            unset($this->cookies[$key]);
418
                        }
419
                    }elseif($key){
420
                        $this->cookies[$key] = $val;
421
                    }
422
                }
423
            }
424
425
            $this->_debug('Object headers',$this->resp_headers);
426
427
            // check server status code to follow redirect
428
            if($this->status == 301 || $this->status == 302 ){
429
                if (empty($this->resp_headers['location'])){
430
                    throw new HTTPClientException('Redirect but no Location Header found');
431
                }elseif($this->redirect_count == $this->max_redirect){
432
                    throw new HTTPClientException('Maximum number of redirects exceeded');
433
                }else{
434
                    // close the connection because we don't handle content retrieval here
435
                    // that's the easiest way to clean up the connection
436
                    fclose($socket);
437
                    unset(self::$connections[$connectionId]);
438
439
                    $this->redirect_count++;
440
                    $this->referer = $url;
441
                    // handle non-RFC-compliant relative redirects
442
                    if (!preg_match('/^http/i', $this->resp_headers['location'])){
443
                        if($this->resp_headers['location'][0] != '/'){
444
                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
445
                                                            dirname($uri['path']).'/'.$this->resp_headers['location'];
446
                        }else{
447
                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
448
                                                            $this->resp_headers['location'];
449
                        }
450
                    }
451
                    // perform redirected request, always via GET (required by RFC)
452
                    return $this->sendRequest($this->resp_headers['location'],array(),'GET');
453
                }
454
            }
455
456
            // check if headers are as expected
457
            if($this->header_regexp && !preg_match($this->header_regexp,$r_headers))
458
                throw new HTTPClientException('The received headers did not match the given regexp');
459
460
            //read body (with chunked encoding if needed)
461
            $r_body    = '';
462
            if((isset($this->resp_headers['transfer-encoding']) && $this->resp_headers['transfer-encoding'] == 'chunked')
463
            || (isset($this->resp_headers['transfer-coding']) && $this->resp_headers['transfer-coding'] == 'chunked')){
464
                $abort = false;
465
                do {
466
                    $chunk_size = '';
467
                    while (preg_match('/^[a-zA-Z0-9]?$/',$byte=$this->_readData($socket,1,'chunk'))){
468
                        // read chunksize until \r
469
                        $chunk_size .= $byte;
470
                        if (strlen($chunk_size) > 128) // set an abritrary limit on the size of chunks
471
                            throw new HTTPClientException('Allowed response size exceeded');
472
                    }
473
                    $this->_readLine($socket, 'chunk');     // readtrailing \n
474
                    $chunk_size = hexdec($chunk_size);
475
476
                    if($this->max_bodysize && $chunk_size+strlen($r_body) > $this->max_bodysize){
477
                        if ($this->max_bodysize_abort)
478
                            throw new HTTPClientException('Allowed response size exceeded');
479
                        $this->error = 'Allowed response size exceeded';
480
                        $chunk_size = $this->max_bodysize - strlen($r_body);
481
                        $abort = true;
482
                    }
483
484
                    if ($chunk_size > 0) {
485
                        $r_body .= $this->_readData($socket, $chunk_size, 'chunk');
486
                        $this->_readData($socket, 2, 'chunk'); // read trailing \r\n
487
                    }
488
                } while ($chunk_size && !$abort);
489
            }elseif(isset($this->resp_headers['content-length']) && !isset($this->resp_headers['transfer-encoding'])){
490
                /* RFC 2616
491
                 * If a message is received with both a Transfer-Encoding header field and a Content-Length
492
                 * header field, the latter MUST be ignored.
493
                 */
494
495
                // read up to the content-length or max_bodysize
496
                // for keep alive we need to read the whole message to clean up the socket for the next read
497
                if(!$this->keep_alive && $this->max_bodysize && $this->max_bodysize < $this->resp_headers['content-length']){
498
                    $length = $this->max_bodysize;
499
                }else{
500
                    $length = $this->resp_headers['content-length'];
501
                }
502
503
                $r_body = $this->_readData($socket, $length, 'response (content-length limited)', true);
504
            }elseif( !isset($this->resp_headers['transfer-encoding']) && $this->max_bodysize && !$this->keep_alive){
505
                $r_body = $this->_readData($socket, $this->max_bodysize, 'response (content-length limited)', true);
506
            }else{
507
                // read entire socket
508
                while (!feof($socket)) {
509
                    $r_body .= $this->_readData($socket, 4096, 'response (unlimited)', true);
510
                }
511
            }
512
513
            // recheck body size, we might had to read the whole body, so we abort late or trim here
514
            if($this->max_bodysize){
515
                if(strlen($r_body) > $this->max_bodysize){
516
                    if ($this->max_bodysize_abort) {
517
                        throw new HTTPClientException('Allowed response size exceeded');
518
                    } else {
519
                        $this->error = 'Allowed response size exceeded';
520
                    }
521
                }
522
            }
523
524
        } catch (HTTPClientException $err) {
525
            $this->error = $err->getMessage();
526
            if ($err->getCode())
527
                $this->status = $err->getCode();
528
            unset(self::$connections[$connectionId]);
529
            fclose($socket);
530
            return false;
531
        }
532
533
        if (!$this->keep_alive ||
534
                (isset($this->resp_headers['connection']) && $this->resp_headers['connection'] == 'Close')) {
535
            // close socket
536
            fclose($socket);
537
            unset(self::$connections[$connectionId]);
538
        }
539
540
        // decode gzip if needed
541
        if(isset($this->resp_headers['content-encoding']) &&
542
           $this->resp_headers['content-encoding'] == 'gzip' &&
543
           strlen($r_body) > 10 && substr($r_body,0,3)=="\x1f\x8b\x08"){
544
            $this->resp_body = @gzinflate(substr($r_body, 10));
545
            if($this->resp_body === false){
546
                $this->error = 'Failed to decompress gzip encoded content';
547
                $this->resp_body = $r_body;
548
            }
549
        }else{
550
            $this->resp_body = $r_body;
551
        }
552
553
        $this->_debug('response body',$this->resp_body);
554
        $this->redirect_count = 0;
555
        return true;
556
    }
557
558
    /**
559
     * Tries to establish a CONNECT tunnel via Proxy
560
     *
561
     * Protocol, Servername and Port will be stripped from the request URL when a successful CONNECT happened
562
     *
563
     * @param resource &$socket
564
     * @param string   &$requesturl
565
     * @throws HTTPClientException when a tunnel is needed but could not be established
566
     * @return bool true if a tunnel was established
567
     */
568
    function _ssltunnel(&$socket, &$requesturl){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
569
        if(!$this->proxy_host) return false;
570
        $requestinfo = parse_url($requesturl);
571
        if($requestinfo['scheme'] != 'https') return false;
572
        if(!$requestinfo['port']) $requestinfo['port'] = 443;
573
574
        // build request
575
        $request  = "CONNECT {$requestinfo['host']}:{$requestinfo['port']} HTTP/1.0".HTTP_NL;
576
        $request .= "Host: {$requestinfo['host']}".HTTP_NL;
577
        if($this->proxy_user) {
578
            $request .= 'Proxy-Authorization: Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass).HTTP_NL;
579
        }
580
        $request .= HTTP_NL;
581
582
        $this->_debug('SSL Tunnel CONNECT',$request);
583
        $this->_sendData($socket, $request, 'SSL Tunnel CONNECT');
584
585
        // read headers from socket
586
        $r_headers = '';
587
        do{
588
            $r_line = $this->_readLine($socket, 'headers');
589
            $r_headers .= $r_line;
590
        }while($r_line != "\r\n" && $r_line != "\n");
591
592
        $this->_debug('SSL Tunnel Response',$r_headers);
593
        if(preg_match('/^HTTP\/1\.[01] 200/i',$r_headers)){
594
            // set correct peer name for verification (enabled since PHP 5.6)
595
            stream_context_set_option($socket, 'ssl', 'peer_name', $requestinfo['host']);
596
597
            // because SSLv3 is mostly broken, we try TLS connections here first.
598
            // according to  https://github.com/splitbrain/dokuwiki/commit/c05ef534 we had problems with certain
599
            // setups with this solution before, but we have no usable test for that and TLS should be the more
600
            // common crypto by now
601
            if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
602
                $requesturl = $requestinfo['path'].
603
                  (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
604
                return true;
605
            }
606
607
            // if the above failed, this will most probably not work either, but we can try
608
            if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_SSLv3_CLIENT)) {
609
                $requesturl = $requestinfo['path'].
610
                  (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
611
                return true;
612
            }
613
614
            throw new HTTPClientException('Failed to set up crypto for secure connection to '.$requestinfo['host'], -151);
615
        }
616
617
        throw new HTTPClientException('Failed to establish secure proxy connection', -150);
618
    }
619
620
    /**
621
     * Safely write data to a socket
622
     *
623
     * @param  resource $socket     An open socket handle
624
     * @param  string   $data       The data to write
625
     * @param  string   $message    Description of what is being read
626
     * @throws HTTPClientException
627
     *
628
     * @author Tom N Harris <[email protected]>
629
     */
630
    function _sendData($socket, $data, $message) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
631
        // send request
632
        $towrite = strlen($data);
633
        $written = 0;
634
        while($written < $towrite){
635
            // check timeout
636
            $time_used = $this->_time() - $this->start;
637
            if($time_used > $this->timeout)
638
                throw new HTTPClientException(sprintf('Timeout while sending %s (%.3fs)',$message, $time_used), -100);
639
            if(feof($socket))
640
                throw new HTTPClientException("Socket disconnected while writing $message");
641
642
            // select parameters
643
            $sel_r = null;
644
            $sel_w = array($socket);
645
            $sel_e = null;
646
            // wait for stream ready or timeout (1sec)
647
            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
648
                 usleep(1000);
649
                 continue;
650
            }
651
652
            // write to stream
653
            $nbytes = fwrite($socket, substr($data,$written,4096));
654
            if($nbytes === false)
655
                throw new HTTPClientException("Failed writing to socket while sending $message", -100);
656
            $written += $nbytes;
657
        }
658
    }
659
660
    /**
661
     * Safely read data from a socket
662
     *
663
     * Reads up to a given number of bytes or throws an exception if the
664
     * response times out or ends prematurely.
665
     *
666
     * @param  resource $socket     An open socket handle in non-blocking mode
667
     * @param  int      $nbytes     Number of bytes to read
668
     * @param  string   $message    Description of what is being read
669
     * @param  bool     $ignore_eof End-of-file is not an error if this is set
670
     * @throws HTTPClientException
671
     * @return string
672
     *
673
     * @author Tom N Harris <[email protected]>
674
     */
675
    function _readData($socket, $nbytes, $message, $ignore_eof = false) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
676
        $r_data = '';
677
        // Does not return immediately so timeout and eof can be checked
678
        if ($nbytes < 0) $nbytes = 0;
679
        $to_read = $nbytes;
680
        do {
681
            $time_used = $this->_time() - $this->start;
682
            if ($time_used > $this->timeout)
683
                throw new HTTPClientException(
684
                        sprintf('Timeout while reading %s after %d bytes (%.3fs)', $message,
685
                                strlen($r_data), $time_used), -100);
686
            if(feof($socket)) {
687
                if(!$ignore_eof)
688
                    throw new HTTPClientException("Premature End of File (socket) while reading $message");
689
                break;
690
            }
691
692
            if ($to_read > 0) {
693
                // select parameters
694
                $sel_r = array($socket);
695
                $sel_w = null;
696
                $sel_e = null;
697
                // wait for stream ready or timeout (1sec)
698
                if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
699
                     usleep(1000);
700
                     continue;
701
                }
702
703
                $bytes = fread($socket, $to_read);
704
                if($bytes === false)
705
                    throw new HTTPClientException("Failed reading from socket while reading $message", -100);
706
                $r_data .= $bytes;
707
                $to_read -= strlen($bytes);
708
            }
709
        } while ($to_read > 0 && strlen($r_data) < $nbytes);
710
        return $r_data;
711
    }
712
713
    /**
714
     * Safely read a \n-terminated line from a socket
715
     *
716
     * Always returns a complete line, including the terminating \n.
717
     *
718
     * @param  resource $socket     An open socket handle in non-blocking mode
719
     * @param  string   $message    Description of what is being read
720
     * @throws HTTPClientException
721
     * @return string
722
     *
723
     * @author Tom N Harris <[email protected]>
724
     */
725
    function _readLine($socket, $message) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
726
        $r_data = '';
727
        do {
728
            $time_used = $this->_time() - $this->start;
729
            if ($time_used > $this->timeout)
730
                throw new HTTPClientException(
731
                        sprintf('Timeout while reading %s (%.3fs) >%s<', $message, $time_used, $r_data),
732
                        -100);
733
            if(feof($socket))
734
                throw new HTTPClientException("Premature End of File (socket) while reading $message");
735
736
            // select parameters
737
            $sel_r = array($socket);
738
            $sel_w = null;
739
            $sel_e = null;
740
            // wait for stream ready or timeout (1sec)
741
            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
742
                 usleep(1000);
743
                 continue;
744
            }
745
746
            $r_data = fgets($socket, 1024);
747
        } while (!preg_match('/\n$/',$r_data));
748
        return $r_data;
749
    }
750
751
    /**
752
     * print debug info
753
     *
754
     * Uses _debug_text or _debug_html depending on the SAPI name
755
     *
756
     * @author Andreas Gohr <[email protected]>
757
     *
758
     * @param string $info
759
     * @param mixed  $var
760
     */
761
    function _debug($info,$var=null){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
762
        if(!$this->debug) return;
763
        if(php_sapi_name() == 'cli'){
764
            $this->_debug_text($info, $var);
765
        }else{
766
            $this->_debug_html($info, $var);
767
        }
768
    }
769
770
    /**
771
     * print debug info as HTML
772
     *
773
     * @param string $info
774
     * @param mixed  $var
775
     */
776
    function _debug_html($info, $var=null){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
777
        print '<b>'.$info.'</b> '.($this->_time() - $this->start).'s<br />';
778
        if(!is_null($var)){
779
            ob_start();
780
            print_r($var);
781
            $content = htmlspecialchars(ob_get_contents());
782
            ob_end_clean();
783
            print '<pre>'.$content.'</pre>';
784
        }
785
    }
786
787
    /**
788
     * prints debug info as plain text
789
     *
790
     * @param string $info
791
     * @param mixed  $var
792
     */
793
    function _debug_text($info, $var=null){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
794
        print '*'.$info.'* '.($this->_time() - $this->start)."s\n";
795
        if(!is_null($var)) print_r($var);
796
        print "\n-----------------------------------------------\n";
797
    }
798
799
    /**
800
     * Return current timestamp in microsecond resolution
801
     *
802
     * @return float
803
     */
804
    static function _time(){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
805
        list($usec, $sec) = explode(" ", microtime());
806
        return ((float)$usec + (float)$sec);
807
    }
808
809
    /**
810
     * convert given header string to Header array
811
     *
812
     * All Keys are lowercased.
813
     *
814
     * @author Andreas Gohr <[email protected]>
815
     *
816
     * @param string $string
817
     * @return array
818
     */
819
    function _parseHeaders($string){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
820
        $headers = array();
821
        $lines = explode("\n",$string);
822
        array_shift($lines); //skip first line (status)
823
        foreach($lines as $line){
824
            @list($key, $val) = explode(':',$line,2);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
825
            $key = trim($key);
826
            $val = trim($val);
827
            $key = strtolower($key);
828
            if(!$key) continue;
829
            if(isset($headers[$key])){
830
                if(is_array($headers[$key])){
831
                    $headers[$key][] = $val;
832
                }else{
833
                    $headers[$key] = array($headers[$key],$val);
834
                }
835
            }else{
836
                $headers[$key] = $val;
837
            }
838
        }
839
        return $headers;
840
    }
841
842
    /**
843
     * convert given header array to header string
844
     *
845
     * @author Andreas Gohr <[email protected]>
846
     *
847
     * @param array $headers
848
     * @return string
849
     */
850
    function _buildHeaders($headers){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
851
        $string = '';
852
        foreach($headers as $key => $value){
853
            if($value === '') continue;
854
            $string .= $key.': '.$value.HTTP_NL;
855
        }
856
        return $string;
857
    }
858
859
    /**
860
     * get cookies as http header string
861
     *
862
     * @author Andreas Goetz <[email protected]>
863
     *
864
     * @return string
865
     */
866
    function _getCookies(){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
867
        $headers = '';
868
        foreach ($this->cookies as $key => $val){
869
            $headers .= "$key=$val; ";
870
        }
871
        $headers = substr($headers, 0, -2);
872
        if ($headers) $headers = "Cookie: $headers".HTTP_NL;
873
        return $headers;
874
    }
875
876
    /**
877
     * Encode data for posting
878
     *
879
     * @author Andreas Gohr <[email protected]>
880
     *
881
     * @param array $data
882
     * @return string
883
     */
884
    function _postEncode($data){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
885
        return http_build_query($data,'','&');
886
    }
887
888
    /**
889
     * Encode data for posting using multipart encoding
890
     *
891
     * @fixme use of urlencode might be wrong here
892
     * @author Andreas Gohr <[email protected]>
893
     *
894
     * @param array $data
895
     * @return string
896
     */
897
    function _postMultipartEncode($data){
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
898
        $boundary = '--'.$this->boundary;
899
        $out = '';
900
        foreach($data as $key => $val){
901
            $out .= $boundary.HTTP_NL;
902
            if(!is_array($val)){
903
                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"'.HTTP_NL;
904
                $out .= HTTP_NL; // end of headers
905
                $out .= $val;
906
                $out .= HTTP_NL;
907
            }else{
908
                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"';
909
                if($val['filename']) $out .= '; filename="'.urlencode($val['filename']).'"';
910
                $out .= HTTP_NL;
911
                if($val['mimetype']) $out .= 'Content-Type: '.$val['mimetype'].HTTP_NL;
912
                $out .= HTTP_NL; // end of headers
913
                $out .= $val['body'];
914
                $out .= HTTP_NL;
915
            }
916
        }
917
        $out .= "$boundary--".HTTP_NL;
918
        return $out;
919
    }
920
921
    /**
922
     * Generates a unique identifier for a connection.
923
     *
924
     * @param  string $server
925
     * @param  string $port
926
     * @return string unique identifier
927
     */
928
    function _uniqueConnectionId($server, $port) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
929
        return "$server:$port";
930
    }
931
}
932
933
//Setup VIM: ex: et ts=4 :
934