Completed
Push — master ( 8b71eb...ddd45f )
by Maik
09:34
created

HttpClient::isOpen()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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