Completed
Pull Request — authpdo (#1572)
by
unknown
04:27
created

HTTPClient::_postEncode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 1
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
        }else{
276
            $request_url = $path;
277
            if (!isset($port)) $port = ($uri['scheme'] == 'https') ? 443 : 80;
278
        }
279
280
        // add SSL stream prefix if needed - needs SSL support in PHP
281
        if($port == 443 || $this->proxy_ssl) {
282
            if(!in_array('ssl', stream_get_transports())) {
283
                $this->status = -200;
284
                $this->error = 'This PHP version does not support SSL - cannot connect to server';
285
            }
286
            $server = 'ssl://'.$server;
287
        }
288
289
        // prepare headers
290
        $headers               = $this->headers;
291
        $headers['Host']       = $uri['host'];
292
        if(!empty($uri['port'])) $headers['Host'].= ':'.$uri['port'];
293
        $headers['User-Agent'] = $this->agent;
294
        $headers['Referer']    = $this->referer;
295
296
        if($method == 'POST'){
297
            if(is_array($data)){
298
                if($headers['Content-Type'] == 'multipart/form-data'){
299
                    $headers['Content-Type']   = 'multipart/form-data; boundary='.$this->boundary;
300
                    $data = $this->_postMultipartEncode($data);
301
                }else{
302
                    $headers['Content-Type']   = 'application/x-www-form-urlencoded';
303
                    $data = $this->_postEncode($data);
304
                }
305
            }
306
            $headers['Content-Length'] = strlen($data);
307
        }elseif($method == 'GET'){
308
            $data = ''; //no data allowed on GET requests
309
        }
310
        if($this->user) {
311
            $headers['Authorization'] = 'Basic '.base64_encode($this->user.':'.$this->pass);
312
        }
313
        if($this->proxy_user) {
314
            $headers['Proxy-Authorization'] = 'Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass);
315
        }
316
317
        // already connected?
318
        $connectionId = $this->_uniqueConnectionId($server,$port);
319
        $this->_debug('connection pool', self::$connections);
320
        $socket = null;
321
        if (isset(self::$connections[$connectionId])) {
322
            $this->_debug('reusing connection', $connectionId);
323
            $socket = self::$connections[$connectionId];
324
        }
325
        if (is_null($socket) || feof($socket)) {
326
            $this->_debug('opening connection', $connectionId);
327
            // open socket
328
            $socket = @fsockopen($server,$port,$errno, $errstr, $this->timeout);
329
            if (!$socket){
330
                $this->status = -100;
331
                $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
332
                return false;
333
            }
334
335
            // try establish a CONNECT tunnel for SSL
336
            try {
337
                if($this->_ssltunnel($socket, $request_url)){
338
                    // no keep alive for tunnels
339
                    $this->keep_alive = false;
340
                    // tunnel is authed already
341
                    if(isset($headers['Proxy-Authentication'])) unset($headers['Proxy-Authentication']);
342
                }
343
            } catch (HTTPClientException $e) {
344
                $this->status = $e->getCode();
345
                $this->error = $e->getMessage();
346
                fclose($socket);
347
                return false;
348
            }
349
350
            // keep alive?
351
            if ($this->keep_alive) {
352
                self::$connections[$connectionId] = $socket;
353
            } else {
354
                unset(self::$connections[$connectionId]);
355
            }
356
        }
357
358
        if ($this->keep_alive && !$this->proxy_host) {
359
            // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
360
            // connection token to a proxy server. We still do keep the connection the
361
            // proxy alive (well except for CONNECT tunnels)
362
            $headers['Connection'] = 'Keep-Alive';
363
        } else {
364
            $headers['Connection'] = 'Close';
365
        }
366
367
        try {
368
            //set non-blocking
369
            stream_set_blocking($socket, 0);
370
371
            // build request
372
            $request  = "$method $request_url HTTP/".$this->http.HTTP_NL;
373
            $request .= $this->_buildHeaders($headers);
374
            $request .= $this->_getCookies();
375
            $request .= HTTP_NL;
376
            $request .= $data;
377
378
            $this->_debug('request',$request);
379
            $this->_sendData($socket, $request, 'request');
380
381
            // read headers from socket
382
            $r_headers = '';
383
            do{
384
                $r_line = $this->_readLine($socket, 'headers');
385
                $r_headers .= $r_line;
386
            }while($r_line != "\r\n" && $r_line != "\n");
387
388
            $this->_debug('response headers',$r_headers);
389
390
            // check if expected body size exceeds allowance
391
            if($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i',$r_headers,$match)){
392
                if($match[1] > $this->max_bodysize){
393
                    if ($this->max_bodysize_abort)
394
                        throw new HTTPClientException('Reported content length exceeds allowed response size');
395
                    else
396
                        $this->error = 'Reported content length exceeds allowed response size';
397
                }
398
            }
399
400
            // get Status
401
            if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/', $r_headers, $m))
402
                throw new HTTPClientException('Server returned bad answer '.$r_headers);
403
404
            $this->status = $m[2];
405
406
            // handle headers and cookies
407
            $this->resp_headers = $this->_parseHeaders($r_headers);
408
            if(isset($this->resp_headers['set-cookie'])){
409
                foreach ((array) $this->resp_headers['set-cookie'] as $cookie){
410
                    list($cookie)   = explode(';',$cookie,2);
411
                    list($key,$val) = explode('=',$cookie,2);
412
                    $key = trim($key);
413
                    if($val == 'deleted'){
414
                        if(isset($this->cookies[$key])){
415
                            unset($this->cookies[$key]);
416
                        }
417
                    }elseif($key){
418
                        $this->cookies[$key] = $val;
419
                    }
420
                }
421
            }
422
423
            $this->_debug('Object headers',$this->resp_headers);
424
425
            // check server status code to follow redirect
426
            if($this->status == 301 || $this->status == 302 ){
427
                if (empty($this->resp_headers['location'])){
428
                    throw new HTTPClientException('Redirect but no Location Header found');
429
                }elseif($this->redirect_count == $this->max_redirect){
430
                    throw new HTTPClientException('Maximum number of redirects exceeded');
431
                }else{
432
                    // close the connection because we don't handle content retrieval here
433
                    // that's the easiest way to clean up the connection
434
                    fclose($socket);
435
                    unset(self::$connections[$connectionId]);
436
437
                    $this->redirect_count++;
438
                    $this->referer = $url;
439
                    // handle non-RFC-compliant relative redirects
440
                    if (!preg_match('/^http/i', $this->resp_headers['location'])){
441
                        if($this->resp_headers['location'][0] != '/'){
442
                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
443
                                                            dirname($uri['path']).'/'.$this->resp_headers['location'];
444
                        }else{
445
                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uri['port'].
446
                                                            $this->resp_headers['location'];
447
                        }
448
                    }
449
                    // perform redirected request, always via GET (required by RFC)
450
                    return $this->sendRequest($this->resp_headers['location'],array(),'GET');
451
                }
452
            }
453
454
            // check if headers are as expected
455
            if($this->header_regexp && !preg_match($this->header_regexp,$r_headers))
456
                throw new HTTPClientException('The received headers did not match the given regexp');
457
458
            //read body (with chunked encoding if needed)
459
            $r_body    = '';
460
            if((isset($this->resp_headers['transfer-encoding']) && $this->resp_headers['transfer-encoding'] == 'chunked')
461
            || (isset($this->resp_headers['transfer-coding']) && $this->resp_headers['transfer-coding'] == 'chunked')){
462
                $abort = false;
463
                do {
464
                    $chunk_size = '';
465
                    while (preg_match('/^[a-zA-Z0-9]?$/',$byte=$this->_readData($socket,1,'chunk'))){
466
                        // read chunksize until \r
467
                        $chunk_size .= $byte;
468
                        if (strlen($chunk_size) > 128) // set an abritrary limit on the size of chunks
469
                            throw new HTTPClientException('Allowed response size exceeded');
470
                    }
471
                    $this->_readLine($socket, 'chunk');     // readtrailing \n
472
                    $chunk_size = hexdec($chunk_size);
473
474
                    if($this->max_bodysize && $chunk_size+strlen($r_body) > $this->max_bodysize){
475
                        if ($this->max_bodysize_abort)
476
                            throw new HTTPClientException('Allowed response size exceeded');
477
                        $this->error = 'Allowed response size exceeded';
478
                        $chunk_size = $this->max_bodysize - strlen($r_body);
479
                        $abort = true;
480
                    }
481
482
                    if ($chunk_size > 0) {
483
                        $r_body .= $this->_readData($socket, $chunk_size, 'chunk');
484
                        $this->_readData($socket, 2, 'chunk'); // read trailing \r\n
485
                    }
486
                } while ($chunk_size && !$abort);
487
            }elseif(isset($this->resp_headers['content-length']) && !isset($this->resp_headers['transfer-encoding'])){
488
                /* RFC 2616
489
                 * If a message is received with both a Transfer-Encoding header field and a Content-Length
490
                 * header field, the latter MUST be ignored.
491
                 */
492
493
                // read up to the content-length or max_bodysize
494
                // for keep alive we need to read the whole message to clean up the socket for the next read
495
                if(!$this->keep_alive && $this->max_bodysize && $this->max_bodysize < $this->resp_headers['content-length']){
496
                    $length = $this->max_bodysize;
497
                }else{
498
                    $length = $this->resp_headers['content-length'];
499
                }
500
501
                $r_body = $this->_readData($socket, $length, 'response (content-length limited)', true);
502
            }elseif( !isset($this->resp_headers['transfer-encoding']) && $this->max_bodysize && !$this->keep_alive){
503
                $r_body = $this->_readData($socket, $this->max_bodysize, 'response (content-length limited)', true);
504
            }else{
505
                // read entire socket
506
                while (!feof($socket)) {
507
                    $r_body .= $this->_readData($socket, 4096, 'response (unlimited)', true);
508
                }
509
            }
510
511
            // recheck body size, we might had to read the whole body, so we abort late or trim here
512
            if($this->max_bodysize){
513
                if(strlen($r_body) > $this->max_bodysize){
514
                    if ($this->max_bodysize_abort) {
515
                        throw new HTTPClientException('Allowed response size exceeded');
516
                    } else {
517
                        $this->error = 'Allowed response size exceeded';
518
                    }
519
                }
520
            }
521
522
        } catch (HTTPClientException $err) {
523
            $this->error = $err->getMessage();
524
            if ($err->getCode())
525
                $this->status = $err->getCode();
526
            unset(self::$connections[$connectionId]);
527
            fclose($socket);
528
            return false;
529
        }
530
531
        if (!$this->keep_alive ||
532
                (isset($this->resp_headers['connection']) && $this->resp_headers['connection'] == 'Close')) {
533
            // close socket
534
            fclose($socket);
535
            unset(self::$connections[$connectionId]);
536
        }
537
538
        // decode gzip if needed
539
        if(isset($this->resp_headers['content-encoding']) &&
540
           $this->resp_headers['content-encoding'] == 'gzip' &&
541
           strlen($r_body) > 10 && substr($r_body,0,3)=="\x1f\x8b\x08"){
542
            $this->resp_body = @gzinflate(substr($r_body, 10));
543
            if($this->resp_body === false){
544
                $this->error = 'Failed to decompress gzip encoded content';
545
                $this->resp_body = $r_body;
546
            }
547
        }else{
548
            $this->resp_body = $r_body;
549
        }
550
551
        $this->_debug('response body',$this->resp_body);
552
        $this->redirect_count = 0;
553
        return true;
554
    }
555
556
    /**
557
     * Tries to establish a CONNECT tunnel via Proxy
558
     *
559
     * Protocol, Servername and Port will be stripped from the request URL when a successful CONNECT happened
560
     *
561
     * @param resource &$socket
562
     * @param string   &$requesturl
563
     * @throws HTTPClientException when a tunnel is needed but could not be established
564
     * @return bool true if a tunnel was established
565
     */
566
    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...
567
        if(!$this->proxy_host) return false;
568
        $requestinfo = parse_url($requesturl);
569
        if($requestinfo['scheme'] != 'https') return false;
570
        if(!$requestinfo['port']) $requestinfo['port'] = 443;
571
572
        // build request
573
        $request  = "CONNECT {$requestinfo['host']}:{$requestinfo['port']} HTTP/1.0".HTTP_NL;
574
        $request .= "Host: {$requestinfo['host']}".HTTP_NL;
575
        if($this->proxy_user) {
576
            $request .= 'Proxy-Authorization: Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass).HTTP_NL;
577
        }
578
        $request .= HTTP_NL;
579
580
        $this->_debug('SSL Tunnel CONNECT',$request);
581
        $this->_sendData($socket, $request, 'SSL Tunnel CONNECT');
582
583
        // read headers from socket
584
        $r_headers = '';
585
        do{
586
            $r_line = $this->_readLine($socket, 'headers');
587
            $r_headers .= $r_line;
588
        }while($r_line != "\r\n" && $r_line != "\n");
589
590
        $this->_debug('SSL Tunnel Response',$r_headers);
591
        if(preg_match('/^HTTP\/1\.[01] 200/i',$r_headers)){
592
            // set correct peer name for verification (enabled since PHP 5.6)
593
            stream_context_set_option($socket, 'ssl', 'peer_name', $requestinfo['host']);
594
595
            // because SSLv3 is mostly broken, we try TLS connections here first.
596
            // according to  https://github.com/splitbrain/dokuwiki/commit/c05ef534 we had problems with certain
597
            // setups with this solution before, but we have no usable test for that and TLS should be the more
598
            // common crypto by now
599
            if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
600
                $requesturl = $requestinfo['path'].
601
                  (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
602
                return true;
603
            }
604
605
            // if the above failed, this will most probably not work either, but we can try
606
            if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_SSLv3_CLIENT)) {
607
                $requesturl = $requestinfo['path'].
608
                  (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
609
                return true;
610
            }
611
612
            throw new HTTPClientException('Failed to set up crypto for secure connection to '.$requestinfo['host'], -151);
613
        }
614
615
        throw new HTTPClientException('Failed to establish secure proxy connection', -150);
616
    }
617
618
    /**
619
     * Safely write data to a socket
620
     *
621
     * @param  resource $socket     An open socket handle
622
     * @param  string   $data       The data to write
623
     * @param  string   $message    Description of what is being read
624
     * @throws HTTPClientException
625
     *
626
     * @author Tom N Harris <[email protected]>
627
     */
628
    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...
629
        // send request
630
        $towrite = strlen($data);
631
        $written = 0;
632
        while($written < $towrite){
633
            // check timeout
634
            $time_used = $this->_time() - $this->start;
635
            if($time_used > $this->timeout)
636
                throw new HTTPClientException(sprintf('Timeout while sending %s (%.3fs)',$message, $time_used), -100);
637
            if(feof($socket))
638
                throw new HTTPClientException("Socket disconnected while writing $message");
639
640
            // select parameters
641
            $sel_r = null;
642
            $sel_w = array($socket);
643
            $sel_e = null;
644
            // wait for stream ready or timeout (1sec)
645
            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
646
                 usleep(1000);
647
                 continue;
648
            }
649
650
            // write to stream
651
            $nbytes = fwrite($socket, substr($data,$written,4096));
652
            if($nbytes === false)
653
                throw new HTTPClientException("Failed writing to socket while sending $message", -100);
654
            $written += $nbytes;
655
        }
656
    }
657
658
    /**
659
     * Safely read data from a socket
660
     *
661
     * Reads up to a given number of bytes or throws an exception if the
662
     * response times out or ends prematurely.
663
     *
664
     * @param  resource $socket     An open socket handle in non-blocking mode
665
     * @param  int      $nbytes     Number of bytes to read
666
     * @param  string   $message    Description of what is being read
667
     * @param  bool     $ignore_eof End-of-file is not an error if this is set
668
     * @throws HTTPClientException
669
     * @return string
670
     *
671
     * @author Tom N Harris <[email protected]>
672
     */
673
    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...
674
        $r_data = '';
675
        // Does not return immediately so timeout and eof can be checked
676
        if ($nbytes < 0) $nbytes = 0;
677
        $to_read = $nbytes;
678
        do {
679
            $time_used = $this->_time() - $this->start;
680
            if ($time_used > $this->timeout)
681
                throw new HTTPClientException(
682
                        sprintf('Timeout while reading %s after %d bytes (%.3fs)', $message,
683
                                strlen($r_data), $time_used), -100);
684
            if(feof($socket)) {
685
                if(!$ignore_eof)
686
                    throw new HTTPClientException("Premature End of File (socket) while reading $message");
687
                break;
688
            }
689
690
            if ($to_read > 0) {
691
                // select parameters
692
                $sel_r = array($socket);
693
                $sel_w = null;
694
                $sel_e = null;
695
                // wait for stream ready or timeout (1sec)
696
                if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
697
                     usleep(1000);
698
                     continue;
699
                }
700
701
                $bytes = fread($socket, $to_read);
702
                if($bytes === false)
703
                    throw new HTTPClientException("Failed reading from socket while reading $message", -100);
704
                $r_data .= $bytes;
705
                $to_read -= strlen($bytes);
706
            }
707
        } while ($to_read > 0 && strlen($r_data) < $nbytes);
708
        return $r_data;
709
    }
710
711
    /**
712
     * Safely read a \n-terminated line from a socket
713
     *
714
     * Always returns a complete line, including the terminating \n.
715
     *
716
     * @param  resource $socket     An open socket handle in non-blocking mode
717
     * @param  string   $message    Description of what is being read
718
     * @throws HTTPClientException
719
     * @return string
720
     *
721
     * @author Tom N Harris <[email protected]>
722
     */
723
    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...
724
        $r_data = '';
725
        do {
726
            $time_used = $this->_time() - $this->start;
727
            if ($time_used > $this->timeout)
728
                throw new HTTPClientException(
729
                        sprintf('Timeout while reading %s (%.3fs) >%s<', $message, $time_used, $r_data),
730
                        -100);
731
            if(feof($socket))
732
                throw new HTTPClientException("Premature End of File (socket) while reading $message");
733
734
            // select parameters
735
            $sel_r = array($socket);
736
            $sel_w = null;
737
            $sel_e = null;
738
            // wait for stream ready or timeout (1sec)
739
            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
740
                 usleep(1000);
741
                 continue;
742
            }
743
744
            $r_data = fgets($socket, 1024);
745
        } while (!preg_match('/\n$/',$r_data));
746
        return $r_data;
747
    }
748
749
    /**
750
     * print debug info
751
     *
752
     * Uses _debug_text or _debug_html depending on the SAPI name
753
     *
754
     * @author Andreas Gohr <[email protected]>
755
     *
756
     * @param string $info
757
     * @param mixed  $var
758
     */
759
    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...
760
        if(!$this->debug) return;
761
        if(php_sapi_name() == 'cli'){
762
            $this->_debug_text($info, $var);
763
        }else{
764
            $this->_debug_html($info, $var);
765
        }
766
    }
767
768
    /**
769
     * print debug info as HTML
770
     *
771
     * @param string $info
772
     * @param mixed  $var
773
     */
774
    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...
775
        print '<b>'.$info.'</b> '.($this->_time() - $this->start).'s<br />';
776
        if(!is_null($var)){
777
            ob_start();
778
            print_r($var);
779
            $content = htmlspecialchars(ob_get_contents());
780
            ob_end_clean();
781
            print '<pre>'.$content.'</pre>';
782
        }
783
    }
784
785
    /**
786
     * prints debug info as plain text
787
     *
788
     * @param string $info
789
     * @param mixed  $var
790
     */
791
    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...
792
        print '*'.$info.'* '.($this->_time() - $this->start)."s\n";
793
        if(!is_null($var)) print_r($var);
794
        print "\n-----------------------------------------------\n";
795
    }
796
797
    /**
798
     * Return current timestamp in microsecond resolution
799
     *
800
     * @return float
801
     */
802
    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...
803
        list($usec, $sec) = explode(" ", microtime());
804
        return ((float)$usec + (float)$sec);
805
    }
806
807
    /**
808
     * convert given header string to Header array
809
     *
810
     * All Keys are lowercased.
811
     *
812
     * @author Andreas Gohr <[email protected]>
813
     *
814
     * @param string $string
815
     * @return array
816
     */
817
    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...
818
        $headers = array();
819
        $lines = explode("\n",$string);
820
        array_shift($lines); //skip first line (status)
821
        foreach($lines as $line){
822
            @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...
823
            $key = trim($key);
824
            $val = trim($val);
825
            $key = strtolower($key);
826
            if(!$key) continue;
827
            if(isset($headers[$key])){
828
                if(is_array($headers[$key])){
829
                    $headers[$key][] = $val;
830
                }else{
831
                    $headers[$key] = array($headers[$key],$val);
832
                }
833
            }else{
834
                $headers[$key] = $val;
835
            }
836
        }
837
        return $headers;
838
    }
839
840
    /**
841
     * convert given header array to header string
842
     *
843
     * @author Andreas Gohr <[email protected]>
844
     *
845
     * @param array $headers
846
     * @return string
847
     */
848
    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...
849
        $string = '';
850
        foreach($headers as $key => $value){
851
            if($value === '') continue;
852
            $string .= $key.': '.$value.HTTP_NL;
853
        }
854
        return $string;
855
    }
856
857
    /**
858
     * get cookies as http header string
859
     *
860
     * @author Andreas Goetz <[email protected]>
861
     *
862
     * @return string
863
     */
864
    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...
865
        $headers = '';
866
        foreach ($this->cookies as $key => $val){
867
            $headers .= "$key=$val; ";
868
        }
869
        $headers = substr($headers, 0, -2);
870
        if ($headers) $headers = "Cookie: $headers".HTTP_NL;
871
        return $headers;
872
    }
873
874
    /**
875
     * Encode data for posting
876
     *
877
     * @author Andreas Gohr <[email protected]>
878
     *
879
     * @param array $data
880
     * @return string
881
     */
882
    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...
883
        return http_build_query($data,'','&');
884
    }
885
886
    /**
887
     * Encode data for posting using multipart encoding
888
     *
889
     * @fixme use of urlencode might be wrong here
890
     * @author Andreas Gohr <[email protected]>
891
     *
892
     * @param array $data
893
     * @return string
894
     */
895
    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...
896
        $boundary = '--'.$this->boundary;
897
        $out = '';
898
        foreach($data as $key => $val){
899
            $out .= $boundary.HTTP_NL;
900
            if(!is_array($val)){
901
                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"'.HTTP_NL;
902
                $out .= HTTP_NL; // end of headers
903
                $out .= $val;
904
                $out .= HTTP_NL;
905
            }else{
906
                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"';
907
                if($val['filename']) $out .= '; filename="'.urlencode($val['filename']).'"';
908
                $out .= HTTP_NL;
909
                if($val['mimetype']) $out .= 'Content-Type: '.$val['mimetype'].HTTP_NL;
910
                $out .= HTTP_NL; // end of headers
911
                $out .= $val['body'];
912
                $out .= HTTP_NL;
913
            }
914
        }
915
        $out .= "$boundary--".HTTP_NL;
916
        return $out;
917
    }
918
919
    /**
920
     * Generates a unique identifier for a connection.
921
     *
922
     * @param  string $server
923
     * @param  string $port
924
     * @return string unique identifier
925
     */
926
    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...
927
        return "$server:$port";
928
    }
929
}
930
931
//Setup VIM: ex: et ts=4 :
932