Failed Conditions
Pull Request — phpunit8 (#3406)
by
unknown
03:15
created

inc/HTTP/HTTPClient.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace dokuwiki\HTTP;
4
5
define('HTTP_NL',"\r\n");
6
7
8
/**
9
 * This class implements a basic HTTP client
10
 *
11
 * It supports POST and GET, Proxy usage, basic authentication,
12
 * handles cookies and referers. It is based upon the httpclient
13
 * function from the VideoDB project.
14
 *
15
 * @link   http://www.splitbrain.org/go/videodb
16
 * @author Andreas Goetz <[email protected]>
17
 * @author Andreas Gohr <[email protected]>
18
 * @author Tobias Sarnowski <[email protected]>
19
 */
20
class HTTPClient {
21
    //set these if you like
22
    public $agent;         // User agent
23
    public $http;          // HTTP version defaults to 1.0
24
    public $timeout;       // read timeout (seconds)
25
    public $cookies;
26
    public $referer;
27
    public $max_redirect;
28
    public $max_bodysize;
29
    public $max_bodysize_abort = true;  // if set, abort if the response body is bigger than max_bodysize
30
    public $header_regexp; // if set this RE must match against the headers, else abort
31
    public $headers;
32
    public $debug;
33
    public $start = 0.0; // for timings
34
    public $keep_alive = true; // keep alive rocks
35
36
    // don't set these, read on error
37
    public $error;
38
    public $redirect_count;
39
40
    // read these after a successful request
41
    public $status;
42
    public $resp_body;
43
    public $resp_headers;
44
45
    // set these to do basic authentication
46
    public $user;
47
    public $pass;
48
49
    // set these if you need to use a proxy
50
    public $proxy_host;
51
    public $proxy_port;
52
    public $proxy_user;
53
    public $proxy_pass;
54
    public $proxy_ssl; //boolean set to true if your proxy needs SSL
55
    public $proxy_except; // regexp of URLs to exclude from proxy
56
57
    // list of kept alive connections
58
    protected static $connections = array();
59
60
    // what we use as boundary on multipart/form-data posts
61
    protected $boundary = '---DokuWikiHTTPClient--4523452351';
62
63
    /**
64
     * Constructor.
65
     *
66
     * @author Andreas Gohr <[email protected]>
67
     */
68
    public function __construct(){
69
        $this->agent        = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; '.PHP_OS.')';
70
        $this->timeout      = 15;
71
        $this->cookies      = array();
72
        $this->referer      = '';
73
        $this->max_redirect = 3;
74
        $this->redirect_count = 0;
75
        $this->status       = 0;
76
        $this->headers      = array();
77
        $this->http         = '1.0';
78
        $this->debug        = false;
79
        $this->max_bodysize = 0;
80
        $this->header_regexp= '';
81
        if(extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
82
        $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,'.
83
            'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
84
        $this->headers['Accept-Language'] = 'en-us';
85
    }
86
87
88
    /**
89
     * Simple function to do a GET request
90
     *
91
     * Returns the wanted page or false on an error;
92
     *
93
     * @param  string $url       The URL to fetch
94
     * @param  bool   $sloppy304 Return body on 304 not modified
95
     * @return false|string  response body, false on error
96
     *
97
     * @author Andreas Gohr <[email protected]>
98
     */
99
    public function get($url,$sloppy304=false){
100
        if(!$this->sendRequest($url)) return false;
101
        if($this->status == 304 && $sloppy304) return $this->resp_body;
102
        if($this->status < 200 || $this->status > 206) return false;
103
        return $this->resp_body;
104
    }
105
106
    /**
107
     * Simple function to do a GET request with given parameters
108
     *
109
     * Returns the wanted page or false on an error.
110
     *
111
     * This is a convenience wrapper around get(). The given parameters
112
     * will be correctly encoded and added to the given base URL.
113
     *
114
     * @param  string $url       The URL to fetch
115
     * @param  array  $data      Associative array of parameters
116
     * @param  bool   $sloppy304 Return body on 304 not modified
117
     * @return false|string  response body, false on error
118
     *
119
     * @author Andreas Gohr <[email protected]>
120
     */
121
    public function dget($url,$data,$sloppy304=false){
122
        if(strpos($url,'?')){
123
            $url .= '&';
124
        }else{
125
            $url .= '?';
126
        }
127
        $url .= $this->postEncode($data);
128
        return $this->get($url,$sloppy304);
129
    }
130
131
    /**
132
     * Simple function to do a POST request
133
     *
134
     * Returns the resulting page or false on an error;
135
     *
136
     * @param  string $url       The URL to fetch
137
     * @param  array  $data      Associative array of parameters
138
     * @return false|string  response body, false on error
139
     * @author Andreas Gohr <[email protected]>
140
     */
141
    public function post($url,$data){
142
        if(!$this->sendRequest($url,$data,'POST')) return false;
143
        if($this->status < 200 || $this->status > 206) return false;
144
        return $this->resp_body;
145
    }
146
147
    /**
148
     * Send an HTTP request
149
     *
150
     * This method handles the whole HTTP communication. It respects set proxy settings,
151
     * builds the request headers, follows redirects and parses the response.
152
     *
153
     * Post data should be passed as associative array. When passed as string it will be
154
     * sent as is. You will need to setup your own Content-Type header then.
155
     *
156
     * @param  string $url    - the complete URL
157
     * @param  mixed  $data   - the post data either as array or raw data
158
     * @param  string $method - HTTP Method usually GET or POST.
159
     * @return bool - true on success
160
     *
161
     * @author Andreas Goetz <[email protected]>
162
     * @author Andreas Gohr <[email protected]>
163
     */
164
    public function sendRequest($url,$data='',$method='GET'){
165
        $this->start  = $this->time();
166
        $this->error  = '';
167
        $this->status = 0;
168
        $this->resp_body = '';
169
        $this->resp_headers = array();
170
171
        // don't accept gzip if truncated bodies might occur
172
        if($this->max_bodysize &&
173
            !$this->max_bodysize_abort &&
174
            $this->headers['Accept-encoding'] == 'gzip'){
175
            unset($this->headers['Accept-encoding']);
176
        }
177
178
        // parse URL into bits
179
        $uri = parse_url($url);
180
        $server = $uri['host'];
181
        $path   = !empty($uri['path']) ? $uri['path'] : '/';
182
        $uriPort = !empty($uri['port']) ? $uri['port'] : null;
183
        if(!empty($uri['query'])) $path .= '?'.$uri['query'];
184
        if(isset($uri['user'])) $this->user = $uri['user'];
185
        if(isset($uri['pass'])) $this->pass = $uri['pass'];
186
187
        // proxy setup
188
        if($this->useProxyForUrl($url)){
189
            $request_url = $url;
190
            $server      = $this->proxy_host;
191
            $port        = $this->proxy_port;
192
            if (empty($port)) $port = 8080;
193
            $use_tls     = $this->proxy_ssl;
194
        }else{
195
            $request_url = $path;
196
            if (!isset($port)) $port = ($uri['scheme'] == 'https') ? 443 : 80;
0 ignored issues
show
The variable $port seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

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