Completed
Push — http-client-refactoring ( f253a2 )
by Vasily
04:08
created

Connection::post()   C

Complexity

Conditions 7
Paths 36

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 7
eloc 18
c 4
b 2
f 0
nc 36
nop 3
dl 0
loc 25
rs 6.7272
1
<?php
2
namespace PHPDaemon\Clients\HTTP;
3
4
use PHPDaemon\Clients\HTTP\Pool;
5
use PHPDaemon\Clients\HTTP\UploadFile;
6
use PHPDaemon\Core\Daemon;
7
use PHPDaemon\Core\Debug;
8
use PHPDaemon\HTTPRequest\Generic;
9
use PHPDaemon\Network\ClientConnection;
10
11
/**
12
 * @package    NetworkClients
13
 * @subpackage HTTPClient
14
 * @author     Vasily Zorin <[email protected]>
15
 */
16
class Connection extends ClientConnection
17
{
18
    /**
19
     * State: headers
20
     */
21
    const STATE_HEADERS = 1;
22
23
    /**
24
     * State: body
25
     */
26
    const STATE_BODY = 2;
27
28
    /**
29
     * @var array Associative array of headers
30
     */
31
    public $headers = [];
32
33
    /**
34
     * @var integer Content length
35
     */
36
    public $contentLength = -1;
37
38
    /**
39
     * @var string Contains response body
40
     */
41
    public $body = '';
42
43
    /**
44
     * @var string End of line
45
     */
46
    protected $EOL = "\r\n";
47
48
    /**
49
     * @var array Associative array of Cookies
50
     */
51
    public $cookie = [];
52
53
    /**
54
     * @var integer Size of current chunk
55
     */
56
    protected $curChunkSize;
57
58
    /**
59
     * @var string
60
     */
61
    protected $curChunk;
62
63
    /**
64
     * @var boolean
65
     */
66
    public $chunked = false;
67
68
    /**
69
     * @var callback
70
     */
71
    public $chunkcb;
72
73
    /**
74
     * @var integer
75
     */
76
    public $protocolError;
77
78
    /**
79
     * @var integer
80
     */
81
    public $responseCode = 0;
82
83
    /**
84
     * @var string Last requested URL
85
     */
86
    public $lastURL;
87
88
    /**
89
     * @var array Raw headers array
90
     */
91
    public $rawHeaders = null;
92
93
    public $contentType;
94
95
    public $charset;
96
97
    public $eofTerminated = false;
98
99
    /**
100
     * @var \SplStack
101
     */
102
    protected $requests;
103
104
    /**
105
     * @var string
106
     */
107
    public $reqType;
108
109
    /**
110
     * Constructor
111
     */
112
    protected function init() {
113
        $this->requests = new \SplStack;
114
    }
115
116
    /**
117
     * Send request headers
118
     * @param $type
119
     * @param $url
120
     * @param &$params
121
     * @return void
122
     */
123
    protected function sendRequestHeaders($type, $url, &$params) {
124
        if (!is_array($params)) {
125
            $params = ['resultcb' => $params];
126
        }
127
        if (!isset($params['uri']) || !isset($params['host'])) {
128
            $prepared = Pool::parseUrl($url);
129
            if (!$prepared) {
130
                if (isset($params['resultcb'])) {
131
                    $params['resultcb'](false);
132
                }
133
                return;
134
            }
135
            list($params['host'], $params['uri']) = $prepared;
136
        }
137
        if ($params['uri'] === '') {
138
            $params['uri'] = '/';
139
        }
140
        $this->lastURL = 'http://' . $params['host'] . $params['uri'];
141
        if (!isset($params['version'])) {
142
            $params['version'] = '1.1';
143
        }
144
        $this->writeln($type . ' ' . $params['uri'] . ' HTTP/' . $params['version']);
145
        if (isset($params['proxy'])) {
146
            if (isset($params['proxy']['auth'])) {
147
                $this->writeln('Proxy-Authorization: basic ' . base64_encode($params['proxy']['auth']['username'] . ':' . $params['proxy']['auth']['password']));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 161 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
148
            }
149
        }
150
        $this->writeln('Host: ' . $params['host']);
151
        if ($this->pool->config->expose->value && !isset($params['headers']['User-Agent'])) {
152
            $this->writeln('User-Agent: phpDaemon/' . Daemon::$version);
153
        }
154
        if (isset($params['cookie']) && sizeof($params['cookie'])) {
155
            $this->writeln('Cookie: ' . http_build_query($params['cookie'], '', '; '));
156
        }
157
        if (isset($params['headers'])) {
158
            $this->customRequestHeaders($params['headers']);
159
        }
160
        if (isset($params['rawHeaders']) && $params['rawHeaders']) {
161
            $this->rawHeaders = [];
162
        }
163
        if (isset($params['chunkcb']) && is_callable($params['chunkcb'])) {
164
            $this->chunkcb = $params['chunkcb'];
165
        }
166
        $this->writeln('');
167
        $this->requests->push($type);
168
        $this->onResponse($params['resultcb']);
169
        $this->checkFree();
170
    }
171
172
    /**
173
     * Perform a HEAD request
174
     * @param string $url
175
     * @param array $params
0 ignored issues
show
Documentation introduced by
Should the type for parameter $params not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
176
     */
177
    public function head($url, $params = null)
178
    {
179
        $this->sendRequestHeaders('HEAD', $url, $params);
180
    }
181
182
    /**
183
     * Perform a GET request
184
     * @param string $url
185
     * @param array $params
0 ignored issues
show
Documentation introduced by
Should the type for parameter $params not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
186
     */
187
    public function get($url, $params = null)
188
    {
189
        $this->sendRequestHeaders('GET', $url, $params);
190
    }
191
192
    /**
193
     * @param array $headers
194
     */
195
    protected function customRequestHeaders($headers)
196
    {
197
        foreach ($headers as $key => $item) {
198
            if (is_numeric($key)) {
199
                if (is_string($item)) {
200
                    $this->writeln($item);
201
                } elseif (is_array($item)) {
202
                    $this->writeln($item[0] . ': ' . $item[1]); // @TODO: prevent injections?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
203
                }
204
            } else {
205
                $this->writeln($key . ': ' . $item);
206
            }
207
        }
208
    }
209
210
    /**
211
     * Perform a POST request
212
     * @param string $url
213
     * @param array $data
214
     * @param array $params
0 ignored issues
show
Documentation introduced by
Should the type for parameter $params not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
215
     */
216
    public function post($url, $data = [], $params = null)
217
    {
218
        foreach ($data as $val) {
219
            if ($val instanceof UploadFile) {
0 ignored issues
show
Bug introduced by
The class PHPDaemon\Clients\HTTP\UploadFile does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
220
                $params['contentType'] = 'multipart/form-data';
221
            }
222
        }
223
        if (!isset($params['contentType'])) {
224
            $params['contentType'] = 'application/x-www-form-urlencoded';
225
        }
226
        if ($params['contentType'] === 'application/x-www-form-urlencoded') {
227
            $body = http_build_query($data, '', '&', PHP_QUERY_RFC3986);
228
        } elseif ($params['contentType'] === 'application/x-json') {
229
            $body = json_encode($data);
230
        } else {
231
            $body = 'Unsupported Content-Type';
232
        }
233
        if (!isset($params['customHeaders'])) {
234
            $params['customHeaders'] = [];
235
        }
236
        $params['customHeaders']['Content-Length'] = mb_orig_strlen($body);
237
        $this->sendRequestHeaders('POST', $url, $params);
238
        $this->write($body);
239
        $this->writeln('');
240
    }
241
242
    /**
243
     * Get body
244
     * @return string
245
     */
246
    public function getBody()
247
    {
248
        return $this->body;
249
    }
250
251
    /**
252
     * Get headers
253
     * @return array
254
     */
255
    public function getHeaders()
256
    {
257
        return $this->headers;
258
    }
259
260
    /**
261
     * Get header
262
     * @param  string $name Header name
263
     * @return string
264
     */
265
    public function getHeader($name)
266
    {
267
        $k = 'HTTP_' . strtoupper(strtr($name, Generic::$htr));
268
        return isset($this->headers[$k]) ? $this->headers[$k] : null;
269
    }
270
271
    /**
272
     * Called when new data received
273
     */
274
    public function onRead()
275
    {
276
        if ($this->state === self::STATE_BODY) {
277
            goto body;
278
        }
279
        if ($this->reqType === null) {
280
            if ($this->requests->isEmpty()) {
281
                $this->finish();
282
                return;
283
            }
284
            $this->reqType = $this->requests->shift();
285
        }
286
        while (($line = $this->readLine()) !== null) {
287
            if ($line !== '') {
288
                if ($this->rawHeaders !== null) {
289
                    $this->rawHeaders[] = $line;
290
                }
291
            } else {
292
                if (isset($this->headers['HTTP_CONTENT_LENGTH'])) {
293
                    $this->contentLength = (int)$this->headers['HTTP_CONTENT_LENGTH'];
294
                } else {
295
                    $this->contentLength = -1;
296
                }
297 View Code Duplication
                if (isset($this->headers['HTTP_TRANSFER_ENCODING'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
                    $e = explode(', ', strtolower($this->headers['HTTP_TRANSFER_ENCODING']));
299
                    $this->chunked = in_array('chunked', $e, true);
300
                } else {
301
                    $this->chunked = false;
302
                }
303 View Code Duplication
                if (isset($this->headers['HTTP_CONNECTION'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
304
                    $e = explode(', ', strtolower($this->headers['HTTP_CONNECTION']));
305
                    $this->keepalive = in_array('keep-alive', $e, true);
306
                }
307
                if (isset($this->headers['HTTP_CONTENT_TYPE'])) {
308
                    parse_str('type=' . strtr($this->headers['HTTP_CONTENT_TYPE'], [';' => '&', ' ' => '']), $p);
309
                    $this->contentType = $p['type'];
310
                    if (isset($p['charset'])) {
311
                        $this->charset = strtolower($p['charset']);
312
                    }
313
                }
314
                if ($this->contentLength === -1 && !$this->chunked && !$this->keepalive) {
315
                    $this->eofTerminated = true;
316
                }
317
                if ($this->reqType === 'HEAD') {
318
                    $this->requestFinished();
319
                } else {
320
                    $this->state = self::STATE_BODY;
321
                }
322
                break;
323
            }
324
            if ($this->state === self::STATE_ROOT) {
325
                $this->headers['STATUS'] = $line;
326
                $e = explode(' ', $this->headers['STATUS']);
327
                $this->responseCode = isset($e[1]) ? (int)$e[1] : 0;
328
                $this->state = self::STATE_HEADERS;
329
            } elseif ($this->state === self::STATE_HEADERS) {
330
                $e = explode(': ', $line);
331
332
                if (isset($e[1])) {
333
                    $k = 'HTTP_' . strtoupper(strtr($e[0], Generic::$htr));
334
                    if ($k === 'HTTP_SET_COOKIE') {
335
                        parse_str(strtr($e[1], [';' => '&', ' ' => '']), $p);
336
                        if (sizeof($p)) {
337
                            $this->cookie[$k = key($p)] =& $p;
338
                            $p['value'] = $p[$k];
339
                            unset($p[$k], $p);
340
                        }
341
                    }
342
                    if (isset($this->headers[$k])) {
343
                        if (is_array($this->headers[$k])) {
344
                            $this->headers[$k][] = $e[1];
345
                        } else {
346
                            $this->headers[$k] = [$this->headers[$k], $e[1]];
347
                        }
348
                    } else {
349
                        $this->headers[$k] = $e[1];
350
                    }
351
                }
352
            }
353
        }
354
        if ($this->state !== self::STATE_BODY) {
355
            return; // not enough data yet
356
        }
357
        body:
358
        if ($this->eofTerminated) {
359
            $body = $this->readUnlimited();
360
            if ($this->chunkcb) {
361
                $func = $this->chunkcb;
362
                $func($body);
363
            }
364
            $this->body .= $body;
365
            return;
366
        }
367
        if ($this->chunked) {
368
            chunk:
369
            if ($this->curChunkSize === null) { // outside of chunk
370
                $l = $this->readLine();
371
                if ($l === '') { // skip empty line
372
                    goto chunk;
373
                }
374
                if ($l === null) {
375
                    return; // not enough data yet
376
                }
377
                if (!ctype_xdigit($l)) {
378
                    $this->protocolError = __LINE__;
379
                    $this->finish(); // protocol error
380
                    return;
381
                }
382
                $this->curChunkSize = hexdec($l);
0 ignored issues
show
Documentation Bug introduced by
It seems like hexdec($l) can also be of type double. However, the property $curChunkSize is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
383
            }
384
            if ($this->curChunkSize !== null) {
385
                if ($this->curChunkSize === 0) {
386
                    if ($this->readLine() === '') {
387
                        $this->requestFinished();
388
                        return;
389
                    } else { // protocol error
390
                        $this->protocolError = __LINE__;
391
                        $this->finish();
392
                        return;
393
                    }
394
                }
395
                $n = $this->curChunkSize - mb_orig_strlen($this->curChunk);
396
                $this->curChunk .= $this->read($n);
397
                if ($this->curChunkSize <= mb_orig_strlen($this->curChunk)) {
398
                    if ($this->chunkcb) {
399
                        $func = $this->chunkcb;
400
                        $func($this->curChunk);
401
                    }
402
                    $this->body .= $this->curChunk;
403
                    $this->curChunkSize = null;
404
                    $this->curChunk = '';
405
                    goto chunk;
406
                }
407
            }
408
        } else {
409
            $body = $this->read($this->contentLength - mb_orig_strlen($this->body));
410
            if ($this->chunkcb) {
411
                $func = $this->chunkcb;
412
                $func($body);
413
            }
414
            $this->body .= $body;
415
            if (($this->contentLength !== -1) && (mb_orig_strlen($this->body) >= $this->contentLength)) {
416
                $this->requestFinished();
417
            }
418
        }
419
    }
420
421
    /**
422
     * Called when connection finishes
423
     */
424
    public function onFinish()
425
    {
426
        if ($this->eofTerminated) {
427
            $this->requestFinished();
428
            $this->onResponse->executeAll($this, false);
429
            parent::onFinish();
430
            return;
431
        }
432
        if ($this->protocolError) {
433
            $this->onResponse->executeAll($this, false);
434
        } else {
435
            if (($this->state !== self::STATE_ROOT) && !$this->onResponse->isEmpty()) {
436
                $this->requestFinished();
437
            }
438
        }
439
        parent::onFinish();
440
    }
441
442
    /**
443
     * Called when request is finished
444
     */
445
    protected function requestFinished()
446
    {
447
        $this->onResponse->executeOne($this, true);
448
        $this->state = self::STATE_ROOT;
449
        $this->contentLength = -1;
450
        $this->curChunkSize = null;
451
        $this->chunked = false;
452
        $this->eofTerminated = false;
453
        $this->headers = [];
454
        $this->rawHeaders = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $rawHeaders.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
455
        $this->contentType = null;
456
        $this->charset = null;
457
        $this->body = '';
458
        $this->responseCode = 0;
459
        $this->reqType = null;
460
        if (!$this->keepalive) {
461
            $this->finish();
462
        }
463
        $this->checkFree();
464
    }
465
}
466