Completed
Push — master ( d1d02b...e3e5bf )
by Maik
02:45
created

HttpClient::request()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.0106

Importance

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