Completed
Push — master ( e3e5bf...e9f06b )
by Maik
02:00
created

HttpClient::request()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

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