Completed
Push — master ( 9cb483...15ad7c )
by Maik
03:24
created

HttpClientTrait::requestImpl()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 13
cts 13
cp 1
rs 8.5125
c 0
b 0
f 0
cc 5
eloc 12
nc 16
nop 1
crap 5
1
<?php
2
namespace Generics\Client;
3
4
use Generics\Streams\InputOutputStream;
5
use Generics\Streams\InputStream;
6
use Generics\Streams\MemoryStream;
7
8
trait HttpClientTrait
9
{
10
    use HttpHeadersTrait;
11
12
    /**
13
     * The query string
14
     *
15
     * @var string
16
     */
17
    private $queryString;
18
19
    /**
20
     * The payload
21
     *
22
     * @var MemoryStream
23
     */
24
    private $payload;
25
26
    /**
27
     * The HTTP protocol version
28
     *
29
     * @var string
30
     */
31
    private $protocol;
32
33
    /**
34
     * Path to file on server (excluding endpoint address)
35
     *
36
     * @var string
37
     */
38
    private $path;
39
40
    /**
41
     * When the connection times out (in seconds)
42
     *
43
     * @var int
44
     */
45
    private $timeout;
46
    
47
    /**
48
     * Load headers from remote and return it
49
     *
50
     * @return array
51
     */
52 2
    public function retrieveHeaders(): array
53
    {
54 2
        $this->setHeader('Connection', 'close');
55 2
        $this->setHeader('Accept', '');
56 2
        $this->setHeader('Accept-Language', '');
57 2
        $this->setHeader('User-Agent', '');
58
        
59 2
        $savedProto = $this->protocol;
60 2
        $this->protocol = 'HTTP/1.0';
61 2
        $this->request('HEAD');
0 ignored issues
show
Bug introduced by
The method request() does not exist on Generics\Client\HttpClientTrait. Did you maybe mean prepareRequest()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
62 2
        $this->protocol = $savedProto;
63
        
64 2
        return $this->getHeaders();
65
    }
66
67
    /**
68
     *
69
     * {@inheritdoc}
70
     * @see \Generics\Streams\HttpStream::appendPayload()
71
     */
72 2
    public function appendPayload(InputStream $payload)
73
    {
74 2
        while ($payload->ready()) {
75 2
            $this->payload->write($payload->read(1024));
76
        }
77 2
    }
78
79
    /**
80
     *
81
     * {@inheritdoc}
82
     * @see \Generics\Streams\HttpStream::getPayload()
83
     */
84 8
    public function getPayload(): InputOutputStream
85
    {
86 8
        return $this->payload;
87
    }
88
89
    /**
90
     * Set connection timeout in seconds
91
     *
92
     * @param int $timeout
93
     */
94 23
    public function setTimeout($timeout)
95
    {
96 23
        $timeout = intval($timeout);
97 23
        if ($timeout < 1 || $timeout > 60) {
98 2
            $timeout = 5;
99
        }
100 23
        $this->timeout = $timeout;
101 23
    }
102
    
103
    /**
104
     *
105
     * {@inheritdoc}
106
     * @see \Generics\Resettable::reset()
107
     */
108 23
    public function reset()
109
    {
110 23
        if (null == $this->payload) {
111 23
            $this->payload = new MemoryStream();
112
        }
113 23
        $this->payload->reset();
114 23
    }
115
116
    /**
117
     * Prepare the request buffer
118
     *
119
     * @param string $requestType
120
     * @return \Generics\Streams\MemoryStream
121
     * @throws \Generics\Streams\StreamException
122
     */
123 22
    private function prepareRequest($requestType): MemoryStream
124
    {
125 22
        $ms = new MemoryStream();
126
        
127
        // First send the request type
128 22
        $ms->interpolate("{rqtype} {path}{query} {proto}\r\n", array(
129 22
            'rqtype' => $requestType,
130 22
            'path' => $this->path,
131 22
            'proto' => $this->protocol,
132 22
            'query' => (strlen($this->queryString) ? '?' . $this->queryString : '')
133
        ));
134
        
135
        // Add the host part
136 22
        $ms->interpolate("Host: {host}\r\n", array(
137 22
            'host' => $this->getEndpoint()
0 ignored issues
show
Bug introduced by
It seems like getEndpoint() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
138 22
                ->getAddress()
139
        ));
140
        
141 22
        $this->adjustHeaders($requestType);
142
        
143
        // Add all existing headers
144 22
        foreach ($this->getHeaders() as $headerName => $headerValue) {
145 22
            if (isset($headerValue) && strlen($headerValue) > 0) {
146 22
                $ms->interpolate("{headerName}: {headerValue}\r\n", array(
147 22
                    'headerName' => $headerName,
148 22
                    'headerValue' => $headerValue
149
                ));
150
            }
151
        }
152
        
153 22
        $ms->write("\r\n");
154
        
155 22
        return $ms;
156
    }
157
158
    /**
159
     * Set the query string
160
     *
161
     * @param string $queryString
162
     */
163 23
    public function setQueryString(string $queryString)
164
    {
165 23
        $this->queryString = $queryString;
166 23
    }
167
168
    /**
169
     * Retrieve and parse the response
170
     *
171
     * @param string $requestType
172
     * @throws \Generics\Client\HttpException
173
     * @throws \Generics\Socket\SocketException
174
     * @throws \Generics\Streams\StreamException
175
     */
176 20
    private function retrieveAndParseResponse($requestType)
177
    {
178 20
        $this->payload = new MemoryStream();
179 20
        $this->headers = array();
180
        
181 20
        $delimiterFound = false;
182
        
183 20
        $tmp = "";
184 20
        $numBytes = 1;
185 20
        $start = time();
186 20
        while (true) {
187 20
            if (! $this->checkConnection($start)) {
188 20
                continue;
189
            }
190
            
191 18
            $c = $this->read($numBytes);
0 ignored issues
show
Bug introduced by
It seems like read() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
192
            
193 18
            if ($c == null) {
194
                break;
195
            }
196
            
197 18
            $start = time(); // we have readen something => adjust timeout start point
198 18
            $tmp .= $c;
199
            
200 18
            if (! $delimiterFound) {
201 18
                $this->handleHeader($delimiterFound, $numBytes, $tmp);
202
            }
203
            
204 18
            if ($delimiterFound) {
205 18
                if ($requestType == 'HEAD') {
206
                    // Header readen, in type HEAD it is now time to leave
207 4
                    break;
208
                }
209
                
210
                // delimiter already found, append to payload
211 14
                $this->payload->write($tmp);
212 14
                $tmp = "";
213
                
214 14
                if ($this->checkContentLengthExceeded()) {
215 14
                    break;
216
                }
217
            }
218
        }
219
        
220
        // Set pointer to start
221 18
        $this->payload->reset();
222
        
223 18
        $mayCompressed = $this->payload->read($this->payload->count());
224 18
        switch ($this->getContentEncoding()) {
225 18
            case 'gzip':
226 2
                $uncompressed = gzdecode(strstr($mayCompressed, "\x1f\x8b"));
227 2
                $this->payload->flush();
228 2
                $this->payload->write($uncompressed);
229 2
                break;
230
            
231 16
            case 'deflate':
232 2
                $uncompressed = gzuncompress($mayCompressed);
233 2
                $this->payload->flush();
234 2
                $this->payload->write($uncompressed);
235 2
                break;
236
            
237
            default:
238
                // nothing
239 14
                break;
240
        }
241 18
        $this->payload->reset();
242 18
    }
243
244
    /**
245
     * Append the payload buffer to the request buffer
246
     *
247
     * @param MemoryStream $ms
248
     * @return MemoryStream
249
     * @throws \Generics\Streams\StreamException
250
     * @throws \Generics\ResetException
251
     */
252 22
    private function appendPayloadToRequest(MemoryStream $ms): MemoryStream
253
    {
254 22
        $this->payload->reset();
255
        
256 22
        while ($this->payload->ready()) {
257 22
            $ms->write($this->payload->read(1024));
258
        }
259
        
260 22
        $ms->reset();
261
        
262 22
        return $ms;
263
    }
264
265
    /**
266
     * Handle a header line
267
     *
268
     * All parameters by reference, which means the the values can be
269
     * modified during execution of this method.
270
     *
271
     * @param boolean $delimiterFound
272
     *            Whether the delimiter for end of header section was found
273
     * @param int $numBytes
274
     *            The number of bytes to read from remote
275
     * @param string $tmp
276
     *            The current readen line
277
     */
278 18
    private function handleHeader(&$delimiterFound, &$numBytes, &$tmp)
279
    {
280 18
        if ($tmp == "\r\n") {
281 18
            $numBytes = $this->adjustNumbytes($numBytes);
282 18
            $delimiterFound = true;
283 18
            $tmp = "";
284 18
            return;
285
        }
286
        
287 18
        if (substr($tmp, - 2, 2) == "\r\n") {
288 18
            $this->addParsedHeader($tmp);
289 18
            $tmp = "";
290
        }
291 18
    }
292
293
    /**
294
     * Perform the request
295
     *
296
     * @param string $requestType
297
     */
298 22
    private function requestImpl(string $requestType)
299
    {
300 22
        if ($requestType == 'HEAD') {
301 4
            $this->setTimeout(1); // Don't wait too long on simple head
302
        }
303
        
304 22
        $ms = $this->prepareRequest($requestType);
305
        
306 22
        $ms = $this->appendPayloadToRequest($ms);
307
        
308 22
        if (! $this->isConnected()) {
0 ignored issues
show
Bug introduced by
It seems like isConnected() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
309 20
            $this->connect();
0 ignored issues
show
Bug introduced by
The method connect() does not exist on Generics\Client\HttpClientTrait. Did you maybe mean checkConnection()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
310
        }
311
        
312 21
        while ($ms->ready()) {
313 21
            $this->write($ms->read(1024));
0 ignored issues
show
Bug introduced by
It seems like write() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
314
        }
315
        
316 20
        $this->retrieveAndParseResponse($requestType);
317
        
318 18
        if ($this->getHeader('Connection') == 'close') {
319 6
            $this->disconnect();
0 ignored issues
show
Bug introduced by
It seems like disconnect() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
320
        }
321 18
    }
322
323
    /**
324
     * Check the connection availability
325
     *
326
     * @param int $start
327
     *            Timestamp when read request attempt starts
328
     * @throws HttpException
329
     * @return bool
330
     */
331 20
    private function checkConnection($start): bool
332
    {
333 20
        if (! $this->ready()) {
0 ignored issues
show
Bug introduced by
It seems like ready() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
334 20
            if (time() - $start > $this->timeout) {
335 2
                $this->disconnect();
0 ignored issues
show
Bug introduced by
It seems like disconnect() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
336 2
                throw new HttpException("Connection timed out!");
337
            }
338
            
339 20
            return false;
340
        }
341
        
342 18
        return true;
343
    }
344
345
    /**
346
     * Check whether the readen bytes amount has reached the
347
     * content length amount
348
     *
349
     * @return bool
350
     */
351 14
    private function checkContentLengthExceeded(): bool
352
    {
353 14
        if (isset($this->headers['Content-Length'])) {
354 14
            if ($this->payload->count() >= $this->headers['Content-Length']) {
355 14
                return true;
356
            }
357
        }
358 14
        return false;
359
    }
360
361
    /**
362
     * Set the used protocol
363
     *
364
     * @param string $protocol
365
     */
366 23
    private function setProtocol(string $protocol)
367
    {
368 23
        $this->protocol = $protocol;
369 23
    }
370
371
    /**
372
     * Set the path on remote server
373
     * 
374
     * @param string $path
375
     */
376 23
    private function setPath(string $path)
377
    {
378 23
        $this->path = $path;
379
    }
380
}