Completed
Push — master ( 4155e6...1dfb62 )
by Vasily
13:45 queued 09:47
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
    {
114
        $this->requests = new \SplStack;
115
    }
116
117
    /**
118
     * Send request headers
119
     * @param $type
120
     * @param $url
121
     * @param &$params
122
     * @return void
123
     */
124
    protected function sendRequestHeaders($type, $url, &$params)
125
    {
126
        if (!is_array($params)) {
127
            $params = ['resultcb' => $params];
128
        }
129
        if (!isset($params['uri']) || !isset($params['host'])) {
130
            $prepared = Pool::parseUrl($url);
131
            if (!$prepared) {
132
                if (isset($params['resultcb'])) {
133
                    $params['resultcb'](false);
134
                }
135
                return;
136
            }
137
            list($params['host'], $params['uri']) = $prepared;
138
        }
139
        if ($params['uri'] === '') {
140
            $params['uri'] = '/';
141
        }
142
        $this->lastURL = 'http://' . $params['host'] . $params['uri'];
143
        if (!isset($params['version'])) {
144
            $params['version'] = '1.1';
145
        }
146
        $this->writeln($type . ' ' . $params['uri'] . ' HTTP/' . $params['version']);
147
        if (isset($params['proxy'])) {
148
            if (isset($params['proxy']['auth'])) {
149
                $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...
150
            }
151
        }
152
        $this->writeln('Host: ' . $params['host']);
153
        if ($this->pool->config->expose->value && !isset($params['headers']['User-Agent'])) {
154
            $this->writeln('User-Agent: phpDaemon/' . Daemon::$version);
155
        }
156
        if (isset($params['cookie']) && sizeof($params['cookie'])) {
157
            $this->writeln('Cookie: ' . http_build_query($params['cookie'], '', '; '));
158
        }
159
        if (isset($params['headers'])) {
160
            $this->customRequestHeaders($params['headers']);
161
        }
162
        if (isset($params['rawHeaders']) && $params['rawHeaders']) {
163
            $this->rawHeaders = [];
164
        }
165
        if (isset($params['chunkcb']) && is_callable($params['chunkcb'])) {
166
            $this->chunkcb = $params['chunkcb'];
167
        }
168
        $this->writeln('');
169
        $this->requests->push($type);
170
        $this->onResponse($params['resultcb']);
171
        $this->checkFree();
172
    }
173
174
    /**
175
     * Perform a HEAD request
176
     * @param string $url
177
     * @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...
178
     */
179
    public function head($url, $params = null)
180
    {
181
        $this->sendRequestHeaders('HEAD', $url, $params);
182
    }
183
184
    /**
185
     * Perform a GET request
186
     * @param string $url
187
     * @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...
188
     */
189
    public function get($url, $params = null)
190
    {
191
        $this->sendRequestHeaders('GET', $url, $params);
192
    }
193
194
    /**
195
     * @param array $headers
196
     */
197
    protected function customRequestHeaders($headers)
198
    {
199
        foreach ($headers as $key => $item) {
200
            if (is_numeric($key)) {
201
                if (is_string($item)) {
202
                    $this->writeln($item);
203
                } elseif (is_array($item)) {
204
                    $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...
205
                }
206
            } else {
207
                $this->writeln($key . ': ' . $item);
208
            }
209
        }
210
    }
211
212
    /**
213
     * Perform a POST request
214
     * @param string $url
215
     * @param array $data
216
     * @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...
217
     */
218
    public function post($url, $data = [], $params = null)
219
    {
220
        foreach ($data as $val) {
221
            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...
222
                $params['contentType'] = 'multipart/form-data';
223
            }
224
        }
225
        if (!isset($params['contentType'])) {
226
            $params['contentType'] = 'application/x-www-form-urlencoded';
227
        }
228
        if ($params['contentType'] === 'application/x-www-form-urlencoded') {
229
            $body = http_build_query($data, '', '&', PHP_QUERY_RFC3986);
230
        } elseif ($params['contentType'] === 'application/x-json') {
231
            $body = json_encode($data);
232
        } else {
233
            $body = 'Unsupported Content-Type';
234
        }
235
        if (!isset($params['customHeaders'])) {
236
            $params['customHeaders'] = [];
237
        }
238
        $params['customHeaders']['Content-Length'] = mb_orig_strlen($body);
239
        $this->sendRequestHeaders('POST', $url, $params);
240
        $this->write($body);
241
        $this->writeln('');
242
    }
243
244
    /**
245
     * Get body
246
     * @return string
247
     */
248
    public function getBody()
249
    {
250
        return $this->body;
251
    }
252
253
    /**
254
     * Get headers
255
     * @return array
256
     */
257
    public function getHeaders()
258
    {
259
        return $this->headers;
260
    }
261
262
    /**
263
     * Get header
264
     * @param  string $name Header name
265
     * @return string
266
     */
267
    public function getHeader($name)
268
    {
269
        $k = 'HTTP_' . strtoupper(strtr($name, Generic::$htr));
270
        return isset($this->headers[$k]) ? $this->headers[$k] : null;
271
    }
272
273
    /**
274
     * Called when new data received
275
     */
276
    public function onRead()
277
    {
278
        if ($this->state === self::STATE_BODY) {
279
            goto body;
280
        }
281
        if ($this->reqType === null) {
282
            if ($this->requests->isEmpty()) {
283
                $this->finish();
284
                return;
285
            }
286
            $this->reqType = $this->requests->shift();
287
        }
288
        while (($line = $this->readLine()) !== null) {
289
            if ($line !== '') {
290
                if ($this->rawHeaders !== null) {
291
                    $this->rawHeaders[] = $line;
292
                }
293
            } else {
294
                if (isset($this->headers['HTTP_CONTENT_LENGTH'])) {
295
                    $this->contentLength = (int)$this->headers['HTTP_CONTENT_LENGTH'];
296
                } else {
297
                    $this->contentLength = -1;
298
                }
299 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...
300
                    $e = explode(', ', strtolower($this->headers['HTTP_TRANSFER_ENCODING']));
301
                    $this->chunked = in_array('chunked', $e, true);
302
                } else {
303
                    $this->chunked = false;
304
                }
305 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...
306
                    $e = explode(', ', strtolower($this->headers['HTTP_CONNECTION']));
307
                    $this->keepalive = in_array('keep-alive', $e, true);
308
                }
309
                if (isset($this->headers['HTTP_CONTENT_TYPE'])) {
310
                    parse_str('type=' . strtr($this->headers['HTTP_CONTENT_TYPE'], [';' => '&', ' ' => '']), $p);
311
                    $this->contentType = $p['type'];
312
                    if (isset($p['charset'])) {
313
                        $this->charset = strtolower($p['charset']);
314
                    }
315
                }
316
                if ($this->contentLength === -1 && !$this->chunked && !$this->keepalive) {
317
                    $this->eofTerminated = true;
318
                }
319
                if ($this->reqType === 'HEAD') {
320
                    $this->requestFinished();
321
                } else {
322
                    $this->state = self::STATE_BODY;
323
                }
324
                break;
325
            }
326
            if ($this->state === self::STATE_ROOT) {
327
                $this->headers['STATUS'] = $line;
328
                $e = explode(' ', $this->headers['STATUS']);
329
                $this->responseCode = isset($e[1]) ? (int)$e[1] : 0;
330
                $this->state = self::STATE_HEADERS;
331
            } elseif ($this->state === self::STATE_HEADERS) {
332
                $e = explode(': ', $line);
333
334
                if (isset($e[1])) {
335
                    $k = 'HTTP_' . strtoupper(strtr($e[0], Generic::$htr));
336
                    if ($k === 'HTTP_SET_COOKIE') {
337
                        parse_str(strtr($e[1], [';' => '&', ' ' => '']), $p);
338
                        if (sizeof($p)) {
339
                            $this->cookie[$k = key($p)] =& $p;
340
                            $p['value'] = $p[$k];
341
                            unset($p[$k], $p);
342
                        }
343
                    }
344
                    if (isset($this->headers[$k])) {
345
                        if (is_array($this->headers[$k])) {
346
                            $this->headers[$k][] = $e[1];
347
                        } else {
348
                            $this->headers[$k] = [$this->headers[$k], $e[1]];
349
                        }
350
                    } else {
351
                        $this->headers[$k] = $e[1];
352
                    }
353
                }
354
            }
355
        }
356
        if ($this->state !== self::STATE_BODY) {
357
            return; // not enough data yet
358
        }
359
        body:
360
        if ($this->eofTerminated) {
361
            $body = $this->readUnlimited();
362
            if ($this->chunkcb) {
363
                $func = $this->chunkcb;
364
                $func($body);
365
            }
366
            $this->body .= $body;
367
            return;
368
        }
369
        if ($this->chunked) {
370
            chunk:
371
            if ($this->curChunkSize === null) { // outside of chunk
372
                $l = $this->readLine();
373
                if ($l === '') { // skip empty line
374
                    goto chunk;
375
                }
376
                if ($l === null) {
377
                    return; // not enough data yet
378
                }
379
                if (!ctype_xdigit($l)) {
380
                    $this->protocolError = __LINE__;
381
                    $this->finish(); // protocol error
382
                    return;
383
                }
384
                $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...
385
            }
386
            if ($this->curChunkSize !== null) {
387
                if ($this->curChunkSize === 0) {
388
                    if ($this->readLine() === '') {
389
                        $this->requestFinished();
390
                        return;
391
                    } else { // protocol error
392
                        $this->protocolError = __LINE__;
393
                        $this->finish();
394
                        return;
395
                    }
396
                }
397
                $n = $this->curChunkSize - mb_orig_strlen($this->curChunk);
398
                $this->curChunk .= $this->read($n);
399
                if ($this->curChunkSize <= mb_orig_strlen($this->curChunk)) {
400
                    if ($this->chunkcb) {
401
                        $func = $this->chunkcb;
402
                        $func($this->curChunk);
403
                    }
404
                    $this->body .= $this->curChunk;
405
                    $this->curChunkSize = null;
406
                    $this->curChunk = '';
407
                    goto chunk;
408
                }
409
            }
410
        } else {
411
            $body = $this->read($this->contentLength - mb_orig_strlen($this->body));
412
            if ($this->chunkcb) {
413
                $func = $this->chunkcb;
414
                $func($body);
415
            }
416
            $this->body .= $body;
417
            if (($this->contentLength !== -1) && (mb_orig_strlen($this->body) >= $this->contentLength)) {
418
                $this->requestFinished();
419
            }
420
        }
421
    }
422
423
    /**
424
     * Called when connection finishes
425
     */
426
    public function onFinish()
427
    {
428
        if ($this->eofTerminated) {
429
            $this->requestFinished();
430
            $this->onResponse->executeAll($this, false);
431
            parent::onFinish();
432
            return;
433
        }
434
        if ($this->protocolError) {
435
            $this->onResponse->executeAll($this, false);
436
        } else {
437
            if (($this->state !== self::STATE_ROOT) && !$this->onResponse->isEmpty()) {
438
                $this->requestFinished();
439
            }
440
        }
441
        parent::onFinish();
442
    }
443
444
    /**
445
     * Called when request is finished
446
     */
447
    protected function requestFinished()
448
    {
449
        $this->onResponse->executeOne($this, true);
450
        $this->state = self::STATE_ROOT;
451
        $this->contentLength = -1;
452
        $this->curChunkSize = null;
453
        $this->chunked = false;
454
        $this->eofTerminated = false;
455
        $this->headers = [];
456
        $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...
457
        $this->contentType = null;
458
        $this->charset = null;
459
        $this->body = '';
460
        $this->responseCode = 0;
461
        $this->reqType = null;
462
        if (!$this->keepalive) {
463
            $this->finish();
464
        }
465
        $this->checkFree();
466
    }
467
}
468