Completed
Push — master ( 039dcf...417ea8 )
by Maik
03:08
created

HttpClientTrait::setQueryString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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