Completed
Push — master ( 7a51d1...ff4d30 )
by Maik
01:43
created

HttpClient::getResponseCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
/**
4
 * This file is part of the PHP Generics package.
5
 *
6
 * @package Generics
7
 */
8
namespace Generics\Client;
9
10
use Generics\Streams\HttpStream;
11
use Generics\Streams\InputStream;
12
use Generics\Streams\MemoryStream;
13
use Generics\Socket\ClientSocket;
14
use Generics\Socket\Url;
15
use Generics\Util\Arrays;
16
17
/**
18
 * This class implements a HttpStream as client
19
 *
20
 * @author Maik Greubel <[email protected]>
21
 */
22
class HttpClient extends ClientSocket implements HttpStream
23
{
24
25
    /**
26
     * Path to file on server (excluding endpoint address)
27
     *
28
     * @var string
29
     */
30
    private $path;
31
32
    /**
33
     * Headers
34
     *
35
     * @var array
36
     */
37
    private $headers;
38
39
    /**
40
     * The payload
41
     *
42
     * @var MemoryStream
43
     */
44
    private $payload;
45
46
    /**
47
     * The HTTP protocol version
48
     *
49
     * @var string
50
     */
51
    private $protocol;
52
53
    /**
54
     * Whether to use https instead of http
55
     *
56
     * @var boolean
57
     */
58
    private $secure;
59
60
    /**
61
     * The response status code
62
     *
63
     * @var int
64
     */
65
    private $responseCode;
66
67
    /**
68
     * When the connection times out (in seconds)
69
     *
70
     * @var int
71
     */
72
    private $timeout;
73
74
    /**
75
     * The query string
76
     *
77
     * @var string
78
     */
79
    private $queryString;
80
81
    /**
82
     * Create a new http client
83
     *
84
     * @param Url $url
85
     *            The url for http request
86
     * @param string $proto
87
     *            The protocol to use (default = HTTP/1.1)
88
     * @param integer $timeout
89
     *            Optional timeout for request (default = 10 seconds)
90
     */
91 11
    public function __construct(Url $url, $proto = 'HTTP/1.1', $timeout = 10)
92
    {
93 11
        parent::__construct($url);
94 11
        $this->path = $url->getPath();
95 11
        $this->queryString = $url->getQueryString();
96 11
        $this->secure = $url->getScheme() == 'https';
97 11
        $this->protocol = $proto;
98 11
        $this->headers = array();
99 11
        $this->payload = new MemoryStream();
100 11
        $this->timeout = $timeout;
101 11
    }
102
103
    /**
104
     *
105
     * {@inheritdoc}
106
     * @see \Generics\Streams\HttpStream::getHeaders()
107
     */
108 2
    public function getHeaders()
109
    {
110 2
        return $this->headers;
111
    }
112
113
    /**
114
     *
115
     * {@inheritdoc}
116
     * @see \Generics\Streams\HttpStream::setHeader()
117
     * @return HttpClient
118
     */
119 10
    public function setHeader($headerName, $headerValue)
120
    {
121 10
        $this->headers[$headerName] = $headerValue;
122 10
        return $this;
123
    }
124
125
    /**
126
     * Reset the headers
127
     */
128 1
    public function resetHeaders()
129
    {
130 1
        $this->headers = array();
131 1
    }
132
133
    /**
134
     *
135
     * {@inheritdoc}
136
     * @see \Generics\Streams\HttpStream::appendPayload()
137
     */
138 1
    public function appendPayload(InputStream $payload)
139
    {
140 1
        while ($payload->ready()) {
141 1
            $this->payload->write($payload->read(1024));
142
        }
143 1
    }
144
145
    /**
146
     *
147
     * {@inheritdoc}
148
     * @see \Generics\Streams\HttpStream::getPayload()
149
     */
150 4
    public function getPayload()
151
    {
152 4
        return $this->payload;
153
    }
154
155
    /**
156
     * Load headers from remote and return it
157
     *
158
     * @return array
159
     */
160 1
    public function retrieveHeaders()
161
    {
162 1
        $this->setHeader('Connection', 'close');
163 1
        $this->setHeader('Accept', '');
164 1
        $this->setHeader('Accept-Language', '');
165 1
        $this->setHeader('User-Agent', '');
166
        
167 1
        $savedProto = $this->protocol;
168 1
        $this->protocol = 'HTTP/1.0';
169 1
        $this->request('HEAD');
170 1
        $this->protocol = $savedProto;
171
        
172 1
        return $this->headers;
173
    }
174
175
    /**
176
     * Set connection timeout in seconds
177
     *
178
     * @param int $timeout
179
     */
180 5
    public function setTimeout($timeout)
181
    {
182 5
        $timeout = intval($timeout);
183 5
        if ($timeout < 1 || $timeout > 60) {
184 1
            $timeout = 5;
185
        }
186 5
        $this->timeout = $timeout;
187 5
    }
188
189
    /**
190
     *
191
     * {@inheritdoc}
192
     * @see \Generics\Streams\HttpStream::request()
193
     */
194 11
    public function request($requestType)
195
    {
196 11
        if ($this->secure) {
197 1
            throw new HttpException("Secure connection using HTTPs is not supported yet!");
198
        }
199
        
200 10
        if ($requestType == 'HEAD') {
201 2
            $this->setTimeout(1); // Don't wait too long on simple head
202
        }
203
        
204 10
        $ms = $this->prepareRequest($requestType);
205
        
206 10
        $ms = $this->appendPayloadToRequest($ms);
207
        
208 10
        if (! $this->isConnected()) {
209 9
            $this->connect();
210
        }
211
        
212 10
        while ($ms->ready()) {
213 10
            $this->write($ms->read(1024));
214
        }
215
        
216 10
        $this->retrieveAndParseResponse($requestType);
217
        
218 9
        if ($this->headers['Connection'] == 'close') {
219 3
            $this->disconnect();
220
        }
221 9
    }
222
223
    /**
224
     * Check the connection availability
225
     *
226
     * @param int $start
227
     *            Timestamp when read request attempt starts
228
     * @throws HttpException
229
     * @return boolean
230
     */
231 10
    private function checkConnection($start)
232
    {
233 10
        if (! $this->ready()) {
234 10
            if (time() - $start > $this->timeout) {
235 1
                $this->disconnect();
236 1
                throw new HttpException("Connection timed out!");
237
            }
238
            
239 10
            return false;
240
        }
241
        
242 9
        return true;
243
    }
244
245
    /**
246
     * Adjust number of bytes to read according content length header
247
     *
248
     * @param int $numBytes
249
     * @return int
250
     */
251 9
    private function adjustNumbytes($numBytes)
252
    {
253 9
        if (isset($this->headers['Content-Length'])) {
254
            // Try to read the whole payload at once
255 9
            $numBytes = intval($this->headers['Content-Length']);
256
        }
257
        
258 9
        return $numBytes;
259
    }
260
261
    /**
262
     * Try to parse line as header and add the results to local header list
263
     *
264
     * @param string $line
265
     */
266 9
    private function addParsedHeader($line)
267
    {
268 9
        if (strpos($line, ':') === false) {
269 9
            $this->responseCode = HttpStatus::parseStatus($line)->getCode();
270
        } else {
271 9
            $line = trim($line);
272 9
            list ($headerName, $headerValue) = explode(':', $line, 2);
273 9
            $this->headers[$headerName] = trim($headerValue);
274
        }
275 9
    }
276
277
    /**
278
     * Check whether the readen bytes amount has reached the
279
     * content length amount
280
     *
281
     * @return boolean
282
     */
283 7
    private function checkContentLengthExceeded()
284
    {
285 7
        if (isset($this->headers['Content-Length'])) {
286 7
            if ($this->payload->count() >= $this->headers['Content-Length']) {
287 7
                return true;
288
            }
289
        }
290 7
        return false;
291
    }
292
293
    /**
294
     * Handle a header line
295
     *
296
     * All parameters by reference, which means the the values can be
297
     * modified during execution of this method.
298
     *
299
     * @param boolean $delimiterFound
300
     *            Whether the delimiter for end of header section was found
301
     * @param int $numBytes
302
     *            The number of bytes to read from remote
303
     * @param string $tmp
304
     *            The current readen line
305
     */
306 9
    private function handleHeader(&$delimiterFound, &$numBytes, &$tmp)
307
    {
308 9
        if ($tmp == "\r\n") {
309 9
            $numBytes = $this->adjustNumbytes($numBytes);
310 9
            $delimiterFound = true;
311 9
            $tmp = "";
312 9
            return;
313
        }
314
        
315 9
        if (substr($tmp, - 2, 2) == "\r\n") {
316 9
            $this->addParsedHeader($tmp);
317 9
            $tmp = "";
318
        }
319 9
    }
320
321
    /**
322
     * Retrieve and parse the response
323
     *
324
     * @param string $requestType
325
     * @throws \Generics\Client\HttpException
326
     * @throws \Generics\Socket\SocketException
327
     * @throws \Generics\Streams\StreamException
328
     */
329 10
    private function retrieveAndParseResponse($requestType)
330
    {
331 10
        $this->payload = new MemoryStream();
332 10
        $this->headers = array();
333
        
334 10
        $delimiterFound = false;
335
        
336 10
        $tmp = "";
337 10
        $numBytes = 1;
338 10
        $start = time();
339 10
        while (true) {
340 10
            if (! $this->checkConnection($start)) {
341 10
                continue;
342
            }
343
            
344 9
            $c = $this->read($numBytes);
345
            
346 9
            if ($c == null) {
347
                break;
348
            }
349
            
350 9
            $start = time(); // we have readen something => adjust timeout start point
351 9
            $tmp .= $c;
352
            
353 9
            if (! $delimiterFound) {
354 9
                $this->handleHeader($delimiterFound, $numBytes, $tmp);
355
            }
356
            
357 9
            if ($delimiterFound) {
358 9
                if ($requestType == 'HEAD') {
359
                    // Header readen, in type HEAD it is now time to leave
360 2
                    break;
361
                }
362
                
363
                // delimiter already found, append to payload
364 7
                $this->payload->write($tmp);
365 7
                $tmp = "";
366
                
367 7
                if ($this->checkContentLengthExceeded()) {
368 7
                    break;
369
                }
370
            }
371
        }
372
        
373
        // Set pointer to start
374 9
        $this->payload->reset();
375
        
376 9
        if(Arrays::hasElement($this->headers, 'Content-Encoding')) {
377 2
            $mayCompressed = $this->payload->read($this->payload->count());
378 2
            switch($this->headers['Content-Encoding']) {
379 2
                case 'gzip': 
380 1
                    $uncompressed = gzdecode(strstr($mayCompressed, "\x1f\x8b"));
381 1
                    $this->payload->flush();
382 1
                    $this->payload->write($uncompressed);
383 1
                    break;
384
                    
385 1
                case 'deflate':
386 1
                    $uncompressed = gzuncompress($mayCompressed);
387 1
                    $this->payload->flush();
388 1
                    $this->payload->write($uncompressed);
389 1
                    break;
390
                    
391
                default:
392
                    // nothing
393
                    break;
394
            }
395
        }
396 9
    }
397
398
    /**
399
     * Append the payload buffer to the request buffer
400
     *
401
     * @param MemoryStream $ms
402
     * @return MemoryStream
403
     * @throws \Generics\Streams\StreamException
404
     * @throws \Generics\ResetException
405
     */
406 10
    private function appendPayloadToRequest(MemoryStream $ms)
407
    {
408 10
        $this->payload->reset();
409
        
410 10
        while ($this->payload->ready()) {
411 10
            $ms->write($this->payload->read(1024));
412
        }
413
        
414 10
        $ms->reset();
415
        
416 10
        return $ms;
417
    }
418
419
    /**
420
     * Prepare the request buffer
421
     *
422
     * @param string $requestType
423
     * @return \Generics\Streams\MemoryStream
424
     * @throws \Generics\Streams\StreamException
425
     */
426 10
    private function prepareRequest($requestType)
427
    {
428 10
        $ms = new MemoryStream();
429
        
430
        // First send the request type
431 10
        $ms->interpolate("{rqtype} {path}{query} {proto}\r\n", array(
432 10
            'rqtype' => $requestType,
433 10
            'path' => $this->path,
434 10
            'proto' => $this->protocol,
435 10
            'query' => (strlen($this->queryString) ? '?' . $this->queryString : '')
436
        ));
437
        
438
        // Add the host part
439 10
        $ms->interpolate("Host: {host}\r\n", array(
440 10
            'host' => $this->getEndpoint()
441 10
                ->getAddress()
442
        ));
443
        
444 10
        $this->adjustHeaders($requestType);
445
        
446
        // Add all existing headers
447 10
        foreach ($this->headers as $headerName => $headerValue) {
448 10
            if (isset($headerValue) && strlen($headerValue) > 0) {
449 10
                $ms->interpolate("{headerName}: {headerValue}\r\n", array(
450 10
                    'headerName' => $headerName,
451 10
                    'headerValue' => $headerValue
452
                ));
453
            }
454
        }
455
        
456 10
        $ms->write("\r\n");
457
        
458 10
        return $ms;
459
    }
460
461
    /**
462
     * Depending on request type the connection header is either
463
     * set to keep-alive or close
464
     *
465
     * @param string $requestType
466
     */
467 8
    private function adjustConnectionHeader($requestType)
468
    {
469 8
        if ($requestType == 'HEAD') {
470 1
            $this->setHeader('Connection', 'close');
471
        } else {
472 7
            $this->setHeader('Connection', 'keep-alive');
473
        }
474 8
    }
475
476
    /**
477
     * Adjust the headers by injecting default values for missing keys.
478
     */
479 10
    private function adjustHeaders($requestType)
480
    {
481 10
        if (! array_key_exists('Accept', $this->headers) && $requestType != 'HEAD') {
482 8
            $this->setHeader('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
483
        }
484
        
485 10
        if (! array_key_exists('Accept-Language', $this->headers) && $requestType != 'HEAD') {
486 8
            $this->setHeader('Accept-Language', 'en-US;q=0.7,en;q=0.3');
487
        }
488
        
489 10
        if (! array_key_exists('User-Agent', $this->headers) && $requestType != 'HEAD') {
490 8
            $this->setHeader('User-Agent', 'phpGenerics 1.0');
491
        }
492
        
493 10
        if (! array_key_exists('Connection', $this->headers) || strlen($this->headers['Connection']) == 0) {
494 8
            $this->adjustConnectionHeader($requestType);
495
        }
496
        
497 10
        if (! array_key_exists('Accept-Encoding', $this->headers)) {
498 10
            $encoding = "";
0 ignored issues
show
Unused Code introduced by
$encoding is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
499 10
            if (function_exists('gzinflate')) {
500 10
                $encoding = 'gzip, deflate';
501
            }
502
            else {
503
                $encoding = 'identity';
504
            }
505 10
            $this->setHeader('Accept-Encoding', $encoding);
506
        }
507 10
    }
508
509
    /**
510
     * Retrieve the response status code
511
     *
512
     * @return int
513
     */
514 8
    public function getResponseCode()
515
    {
516 8
        return $this->responseCode;
517
    }
518
519
    /**
520
     *
521
     * {@inheritdoc}
522
     * @see \Generics\Streams\Stream::isOpen()
523
     */
524
    public function isOpen()
525
    {
526
        return true;
527
    }
528
529
    /**
530
     *
531
     * {@inheritdoc}
532
     * @see \Generics\Resettable::reset()
533
     */
534
    public function reset()
535
    {
536
        $this->payload->reset();
537
    }
538
}
539