Generic   F
last analyzed

Complexity

Total Complexity 168

Size/Duplication

Total Lines 959
Duplicated Lines 10.95 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 105
loc 959
rs 1.641
c 0
b 0
f 0
wmc 168
lcom 1
cbo 10

32 Methods

Rating   Name   Duplication   Size   Complexity  
A preinit() 0 20 3
A firstDeferredEventUsed() 0 4 1
B sendfile() 0 48 8
A getCookieStr() 0 4 1
B checkIfReady() 0 25 8
A getUploadMaxSize() 0 4 1
F parseParams() 6 61 19
D postPrepare() 0 58 20
F ensureSentHeaders() 0 45 15
C out() 0 51 11
A onParsedParams() 0 3 1
A combinedOut() 0 19 4
A chunked() 0 5 1
A onWakeup() 0 14 2
A onSleep() 0 14 2
A status() 0 8 2
A headersSent() 0 6 1
A headersList() 0 4 1
A setcookie() 9 19 6
F header() 57 57 17
A removeHeader() 0 4 1
B parseSize() 33 34 9
A onUploadFileStart() 0 14 2
A onUploadFileChunk() 0 24 5
A freezeInput() 0 5 1
A unfreezeInput() 0 7 2
A getUploadTempDir() 0 7 2
A isUploadedFile() 0 15 5
A moveUploadedFile() 0 7 2
A readBodyFile() 0 22 3
A parseStr() 0 18 5
B postFinishHandler() 0 19 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Generic often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Generic, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace PHPDaemon\HTTPRequest;
3
4
use PHPDaemon\Core\Daemon;
5
use PHPDaemon\FS\File;
6
use PHPDaemon\FS\FileSystem;
7
use PHPDaemon\Request\RequestHeadersAlreadySent;
8
use PHPDaemon\Traits\DeferredEventHandlers;
9
use PHPDaemon\Utils\MIME;
10
11
/**
12
 * HTTP request
13
 * @package PHPDaemon\HTTPRequest
14
 * @author  Vasily Zorin <[email protected]>
15
 */
16
abstract class Generic extends \PHPDaemon\Request\Generic
17
{
18
    use DeferredEventHandlers;
19
    use \PHPDaemon\Traits\Sessions;
20
21
    /**
22
     * @var array Status codes
23
     */
24
    protected static $codes = [
25
        100 => 'Continue',
26
        101 => 'Switching Protocols',
27
        200 => 'OK',
28
        201 => 'Created',
29
        202 => 'Accepted',
30
        203 => 'Non-Authoritative Information',
31
        204 => 'No Content',
32
        205 => 'Reset Content',
33
        206 => 'Partial Content',
34
        300 => 'Multiple Choices',
35
        301 => 'Moved Permanently',
36
        302 => 'Found',
37
        303 => 'See Other',
38
        304 => 'Not Modified',
39
        305 => 'Use Proxy',
40
        306 => '(Unused)',
41
        307 => 'Temporary Redirect',
42
        400 => 'Bad Request',
43
        401 => 'Unauthorized',
44
        402 => 'Payment Required',
45
        403 => 'Forbidden',
46
        404 => 'Not Found',
47
        405 => 'Method Not Allowed',
48
        406 => 'Not Acceptable',
49
        407 => 'Proxy Authentication Required',
50
        408 => 'Request Timeout',
51
        409 => 'Conflict',
52
        410 => 'Gone',
53
        411 => 'Length Required',
54
        412 => 'Precondition Failed',
55
        413 => 'Request Entity Too Large',
56
        414 => 'Request-URI Too Long',
57
        415 => 'Unsupported Media Type',
58
        416 => 'Requested Range Not Satisfiable',
59
        417 => 'Expectation Failed',
60
        422 => 'Unprocessable Entity',
61
        423 => 'Locked',
62
        500 => 'Internal Server Error',
63
        501 => 'Not Implemented',
64
        502 => 'Bad Gateway',
65
        503 => 'Service Unavailable',
66
        504 => 'Gateway Timeout',
67
        505 => 'HTTP Version Not Supported',
68
    ];
69
70
    /**
71
     * @var boolean Keepalive?
72
     */
73
    public $keepalive = false;
74
75
    /**
76
     * @var integer Current response length
77
     */
78
    public $responseLength = 0;
79
80
    /**
81
     * @var integer Content length from header() method
82
     */
83
    protected $contentLength;
84
85
    /**
86
     * @var integer Number of outgoing cookie-headers
87
     */
88
    protected $cookieNum = 0;
89
90
    /**
91
     * @var array Replacement pairs for processing some header values in parse_str()
92
     */
93
    public static $hvaltr = ['; ' => '&', ';' => '&', ' ' => '%20'];
94
95
    /**
96
     * @var array State
97
     */
98
    public static $htr = ['-' => '_'];
99
100
    /**
101
     * @var array Outgoing headers
102
     */
103
    protected $headers = ['STATUS' => '200 OK'];
104
105
    /**
106
     * @var boolean Headers sent?
107
     */
108
    protected $headers_sent = false;
109
110
    /**
111
     * @var boolean File name where output started in the file and line variables
112
     */
113
    protected $headers_sent_file;
114
115
    /**
116
     * @var boolean Line number where output started in the file and line variables
117
     */
118
    protected $headers_sent_line;
119
120
    /**
121
     * @var File File pointer to send output (X-Sendfile)
122
     */
123
    protected $sendfp;
124
125
    /**
126
     * @var boolean Frozen input?
127
     */
128
    protected $frozenInput = false;
129
130
    /**
131
     * @var array Content type parameters
132
     */
133
    protected $contype;
134
135
    /**
136
     * Preparing before init
137
     * @param  object $req Source request
138
     * @return void
139
     */
140
    protected function preinit($req)
141
    {
142
        if ($req === null) {
143
            $req = new \stdClass;
144
            $req->attrs = new \stdClass;
145
            $req->attrs->inputDone = true;
146
            $req->attrs->paramsDone = true;
147
            $req->attrs->chunked = false;
148
        }
149
150
        $this->attrs = $req->attrs;
151
152
        if ($this->upstream->pool->config->expose->value) {
153
            $this->header('X-Powered-By: phpDaemon/' . Daemon::$version);
154
        }
155
156
        $this->attrs->input->setRequest($this);
157
158
        $this->parseParams();
159
    }
160
161
    /**
162
     * Called when first deferred event used
163
     * @return void
164
     */
165
    public function firstDeferredEventUsed()
166
    {
167
        $this->bind('finish', [$this, 'cleanupDeferredEventHandlers']);
168
    }
169
170
    /**
171
     * Output whole contents of file
172
     * @param  string $path Path
173
     * @param  callable $cb Callback
174
     * @param  integer $pri Priority
175
     * @return boolean        Success
176
     */
177
    public function sendfile($path, $cb, $pri = EIO_PRI_DEFAULT)
178
    {
179
        if ($this->state === self::STATE_FINISHED) {
180
            return false;
181
        }
182
        try {
183
            $this->header('Content-Type: ' . MIME::get($path));
184
        } catch (RequestHeadersAlreadySent $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
185
        }
186
        if ($this->upstream->checkSendfileCap()) {
187
            FileSystem::sendfile($this->upstream, $path, $cb, function ($file, $length, $handler) {
188
                try {
189
                    $this->header('Content-Length: ' . $length);
190
                } catch (RequestHeadersAlreadySent $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
191
                }
192
                $this->ensureSentHeaders();
193
                $this->upstream->onWriteOnce(function ($conn) use ($handler, $file) {
0 ignored issues
show
Unused Code introduced by
The parameter $conn is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
194
                    $handler($file);
195
                });
196
                return true;
197
            }, 0, null, $pri);
198
            return true;
199
        }
200
        $first = true;
201
202
        FileSystem::readfileChunked(
203
            $path,
204
            $cb,
205
            function ($file, $chunk) use (&$first) {
206
                // readed chunk
207
                if ($this->upstream->isFreed()) {
208
                    return false;
209
                }
210
211
                if ($first) {
212
                    try {
213
                        $this->header('Content-Length: ' . $file->stat['size']);
214
                    } catch (RequestHeadersAlreadySent $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
215
                    }
216
                    $first = false;
217
                }
218
                $this->out($chunk);
219
                return true;
220
            }
221
        );
222
223
        return true;
224
    }
225
226
    /**
227
     * Get cookie by name
228
     * @param  string $name Name of cookie
229
     * @return string       Contents
230
     */
231
    protected function getCookieStr($name)
232
    {
233
        return static::getString($this->attrs->cookie[$name]);
234
    }
235
236
    /**
237
     * Called to check if Request is ready
238
     * @return boolean Ready?
239
     */
240
    public function checkIfReady()
241
    {
242
        if (!$this->attrs->paramsDone || !$this->attrs->inputDone) {
243
            return false;
244
        }
245
        if (isset($this->appInstance->passphrase)) {
246
            if (!isset($this->attrs->server['PASSPHRASE'])
247
                || ($this->appInstance->passphrase !== $this->attrs->server['PASSPHRASE'])
248
            ) {
249
                $this->finish();
250
            }
251
252
            return false;
253
        }
254
255
        if ($this->attrs->input->isFrozen()) {
256
            return false;
257
        }
258
259
        if ($this->sleepTime === 0) {
260
            $this->wakeup();
261
        }
262
263
        return true;
264
    }
265
266
    /**
267
     * Upload maximum file size
268
     * @return integer
269
     */
270
    public function getUploadMaxSize()
271
    {
272
        return $this->upstream->pool->config->uploadmaxsize->value;
273
    }
274
275
    /**
276
     * Parses GET-query string and other request's headers
277
     * @return void
278
     */
279
    protected function parseParams()
280
    {
281
        if (!isset($this->attrs->server['HTTP_CONTENT_LENGTH'])) {
282
            $this->attrs->contentLength = 0;
283
        } else {
284
            $this->attrs->contentLength = (int)$this->attrs->server['HTTP_CONTENT_LENGTH'];
285
        }
286
287
        if (isset($this->attrs->server['CONTENT_TYPE']) && !isset($this->attrs->server['HTTP_CONTENT_TYPE'])) {
288
            $this->attrs->server['HTTP_CONTENT_TYPE'] = $this->attrs->server['CONTENT_TYPE'];
289
        }
290
291 View Code Duplication
        if (isset($this->attrs->server['QUERY_STRING'])) {
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...
292
            self::parseStr($this->attrs->server['QUERY_STRING'], $this->attrs->get);
293
        }
294
        if (isset($this->attrs->server['REQUEST_METHOD'])
295
            && ($this->attrs->server['REQUEST_METHOD'] === 'POST' || $this->attrs->server['REQUEST_METHOD'] === 'PUT')
296
            && isset($this->attrs->server['HTTP_CONTENT_TYPE'])
297
        ) {
298
            $this->attrs->server['REQUEST_METHOD_POST'] = true;
299
            self::parseStr($this->attrs->server['HTTP_CONTENT_TYPE'], $this->contype, true);
300
            $found = false;
301
            foreach ($this->contype as $k => $v) {
302
                if (mb_orig_strpos($k, '/') === false) {
303
                    continue;
304
                }
305
                if (!$found) {
306
                    $found = true;
307
                } else {
308
                    unset($this->contype[$k]);
309
                }
310
            }
311
312
            if (isset($this->contype['multipart/form-data'])
313
                && (isset($this->contype['boundary']))
314
            ) {
315
                $this->attrs->input->setBoundary($this->contype['boundary']);
316
            }
317
        } else {
318
            $this->attrs->server['REQUEST_METHOD_POST'] = false;
319
        }
320
321 View Code Duplication
        if (isset($this->attrs->server['HTTP_COOKIE'])) {
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...
322
            self::parseStr($this->attrs->server['HTTP_COOKIE'], $this->attrs->cookie, true);
0 ignored issues
show
Documentation introduced by
$this->attrs->server['HTTP_COOKIE'] is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
323
        }
324
325
        if (isset($this->attrs->server['HTTP_AUTHORIZATION'])) {
326
            $e = explode(' ', $this->attrs->server['HTTP_AUTHORIZATION'], 2);
327
328
            if (($e[0] === 'Basic') && isset($e[1])) {
329
                $e[1] = base64_decode($e[1]);
330
                $e = explode(':', $e[1], 2);
331
332
                if (isset($e[1])) {
333
                    list($this->attrs->server['PHP_AUTH_USER'], $this->attrs->server['PHP_AUTH_PW']) = $e;
334
                }
335
            }
336
        }
337
338
        $this->onParsedParams();
339
    }
340
341
    /**
342
     * Prepares the request body
343
     * @return void
344
     */
345
    public function postPrepare()
346
    {
347
        if (!$this->attrs->server['REQUEST_METHOD_POST']) {
348
            return;
349
        }
350
        if (isset($this->attrs->server['REQUEST_PREPARED_UPLOADS']) && $this->attrs->server['REQUEST_PREPARED_UPLOADS'] === 'nginx') {
351
            if (isset($this->attrs->server['REQUEST_PREPARED_UPLOADS_URL_PREFIX'])) {
352
                $URLprefix = $this->attrs->server['REQUEST_PREPARED_UPLOADS_URL_PREFIX'];
353
                $l = mb_orig_strlen($URLprefix);
354
                foreach (['PHP_SELF', 'REQUEST_URI', 'SCRIPT_NAME', 'DOCUMENT_URI'] as $k) {
355
                    if (!isset($this->attrs->server[$k])) {
356
                        continue;
357
                    }
358
                    if (strncmp($this->attrs->server[$k], $URLprefix, $l) === 0) {
359
                        $this->attrs->server[$k] = substr($this->attrs->server[$k], $l - 1);
360
                    }
361
                }
362
            }
363
            $prefix = 'file.';
364
            $prefixlen = mb_orig_strlen($prefix);
365
            foreach ($this->attrs->post as $k => $v) {
366
                if (strncmp($k, $prefix, $prefixlen) === 0) {
367
                    $e = explode('.', substr($k, $prefixlen));
368
                    if (!isset($e[1])) {
369
                        $e = ['file', $e[0]];
370
                    }
371
                    if (!isset($this->attrs->files[$e[0]])) {
372
                        $this->attrs->files[$e[0]] = ['error' => UPLOAD_ERR_OK];
373
                    }
374
                    $this->attrs->files[$e[0]][$e[1]] = $v;
375
                    unset($this->attrs->post[$k]);
376
                }
377
            }
378
            $uploadTmp = $this->getUploadTempDir();
379
            foreach ($this->attrs->files as $k => &$file) {
380
                if (!isset($file['tmp_name'])
381
                    || !isset($file['name'])
382
                    || !ctype_digit(basename($file['tmp_name']))
383
                    || (mb_orig_strpos(pathinfo($file['tmp_name'], PATHINFO_DIRNAME), $uploadTmp) !== 0)
384
                ) {
385
                    unset($this->attrs->files[$k]);
386
                    continue;
387
                }
388
                FileSystem::open($file['tmp_name'], 'c+!', function ($fp) use (&$file) {
389
                    if (!$fp) {
390
                        return;
391
                    }
392
                    $file['fp'] = $fp;
393
                });
394
            }
395
            unset($file);
396
        }
397
        if (isset($this->attrs->server['REQUEST_BODY_FILE'])
398
            && $this->upstream->pool->config->autoreadbodyfile->value
399
        ) {
400
            $this->readBodyFile();
401
        }
402
    }
403
404
    /**
405
     * Ensure that headers are sent
406
     * @return boolean Were already sent?
407
     */
408
    public function ensureSentHeaders()
409
    {
410
        if ($this->headers_sent) {
411
            return true;
412
        }
413
        if (isset($this->headers['STATUS'])) {
414
            $h = (isset($this->attrs->noHttpVer) && ($this->attrs->noHttpVer) ? 'Status: ' : $this->attrs->server['SERVER_PROTOCOL']) . ' ' . $this->headers['STATUS'] . "\r\n";
415
        } else {
416
            $h = '';
417
        }
418
        $http11 = $this->attrs->server['SERVER_PROTOCOL'] === 'HTTP/1.1';
419
        if ($this->contentLength === null
420
            && $this->upstream->checkChunkedEncCap()
421
            && $http11
422
        ) {
423
            $this->attrs->chunked = true;
424
        }
425
        if ($this->attrs->chunked) {
426
            $this->header('Transfer-Encoding: chunked');
427
        }
428
429
        if ($http11) {
430
            $connection = isset($this->attrs->server['HTTP_CONNECTION']) ? strtolower($this->attrs->server['HTTP_CONNECTION']) : 'keep-alive';
431
            if ($connection === 'keep-alive' && $this->upstream->getKeepaliveTimeout() > 0) {
432
                $this->header('Connection: keep-alive');
433
                $this->keepalive = true;
434
            } else {
435
                $this->header('Connection: close');
436
            }
437
        } else {
438
            $this->header('Connection: close');
439
        }
440
441
        foreach ($this->headers as $k => $line) {
442
            if ($k !== 'STATUS') {
443
                $h .= $line . "\r\n";
444
            }
445
        }
446
        $h .= "\r\n";
447
        $this->headers_sent_file = __FILE__;
0 ignored issues
show
Documentation Bug introduced by
The property $headers_sent_file was declared of type boolean, but __FILE__ is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
448
        $this->headers_sent_line = __LINE__;
0 ignored issues
show
Documentation Bug introduced by
The property $headers_sent_line was declared of type boolean, but __LINE__ is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
449
        $this->headers_sent = true;
450
        $this->upstream->requestOut($this, $h);
451
        return false;
452
    }
453
454
    /**
455
     * Output some data
456
     * @param  string $s String to out
457
     * @param  boolean $flush ob_flush?
458
     * @return boolean        Success
459
     */
460
    public function out($s, $flush = true)
461
    {
462
        if ($flush) {
463
            if (!Daemon::$obInStack) { // preventing recursion
464
                ob_flush();
465
            }
466
        }
467
468
        if ($this->aborted) {
469
            return false;
470
        }
471
        if (!isset($this->upstream)) {
472
            return false;
473
        }
474
475
        $l = mb_orig_strlen($s);
476
        $this->responseLength += $l;
477
478
        $this->ensureSentHeaders();
479
480
        if ($this->attrs->chunked) {
481
            for ($o = 0; $o < $l;) {
482
                $c = min($this->upstream->pool->config->chunksize->value, $l - $o);
483
484
                $chunk = dechex($c) . "\r\n"
485
                    . ($c === $l ? $s : mb_orig_substr($s, $o, $c)) // content
486
                    . "\r\n";
487
488
                if ($this->sendfp) {
489
                    $this->sendfp->write($chunk);
490
                } else {
491
                    $this->upstream->requestOut($this, $chunk);
492
                }
493
494
                $o += $c;
495
            }
496
            return true;
497
        } else {
498
            if ($this->sendfp) {
499
                $this->sendfp->write($s);
500
                return true;
501
            }
502
503
            if (Daemon::$compatMode) {
504
                echo $s;
505
                return true;
506
            }
507
508
            return $this->upstream->requestOut($this, $s);
509
        }
510
    }
511
512
    /**
513
     * Called when request's headers parsed
514
     * @return void
515
     */
516
    public function onParsedParams()
517
    {
518
    }
519
520
    /**
521
     * Outputs data with headers (split by \r\n\r\n)
522
     * @param  string $s Data
523
     * @return boolean    Success
524
     */
525
    public function combinedOut($s)
526
    {
527
        if (!$this->headers_sent) {
528
            $e = explode("\r\n\r\n", $s, 2);
529
            $h = explode("\r\n", $e[0]);
530
531
            foreach ($h as $l) {
532
                $this->header($l);
533
            }
534
535
            if (isset($e[1])) {
536
                return $this->out($e[1]);
537
            }
538
539
            return true;
540
        } else {
541
            return $this->out($s);
542
        }
543
    }
544
545
    /**
546
     * Use chunked encoding
547
     * @return void
548
     */
549
    public function chunked()
550
    {
551
        $this->header('Transfer-Encoding: chunked');
552
        $this->attrs->chunked = true;
553
    }
554
555
    /**
556
     * Called when the request wakes up
557
     * @return void
558
     */
559
    public function onWakeup()
560
    {
561
        parent::onWakeup();
562
        if (!Daemon::$obInStack) { // preventing recursion
563
            @ob_flush();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
564
        }
565
        $_GET = &$this->attrs->get;
566
        $_POST = &$this->attrs->post;
567
        $_COOKIE = &$this->attrs->cookie;
568
        $_REQUEST = &$this->attrs->request;
569
        $_SESSION = &$this->attrs->session;
570
        $_FILES = &$this->attrs->files;
571
        $_SERVER = &$this->attrs->server;
572
    }
573
574
    /**
575
     * Called when the request starts sleep
576
     * @return void
577
     */
578
    public function onSleep()
579
    {
580
        if (!Daemon::$obInStack) { // preventing recursion
581
            @ob_flush();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
582
        }
583
        unset($_GET);
584
        unset($_POST);
585
        unset($_COOKIE);
586
        unset($_REQUEST);
587
        unset($_SESSION);
588
        unset($_FILES);
589
        unset($_SERVER);
590
        parent::onSleep();
591
    }
592
593
    /**
594
     * Send HTTP-status
595
     * @param  integer $code Code
596
     * @throws RequestHeadersAlreadySent
597
     * @return boolean Success
598
     */
599
    public function status($code = 200)
600
    {
601
        if (!isset(self::$codes[$code])) {
602
            return false;
603
        }
604
        $this->header($code . ' ' . self::$codes[$code]);
605
        return true;
606
    }
607
608
    /**
609
     * Checks if headers have been sent
610
     * @param  string &$file File name
611
     * @param  integer &$line Line in file
612
     * @return boolean        Success
613
     */
614
    public function headersSent(&$file, &$line)
615
    {
616
        $file = $this->headers_sent_file;
617
        $line = $this->headers_sent_line;
618
        return $this->headers_sent;
619
    }
620
621
    /**
622
     * Return current list of headers
623
     * @return array Headers
624
     */
625
    public function headersList()
626
    {
627
        return array_values($this->headers);
628
    }
629
630
    /**
631
     * Set the cookie
632
     * @param string $name Name of cookie
633
     * @param string $value Value
634
     * @param integer $maxage Optional. Max-Age. Default is 0
635
     * @param string $path Optional. Path. Default is empty string
636
     * @param string $domain Optional. Domain. Default is empty string
637
     * @param boolean $secure Optional. Secure. Default is false
638
     * @param boolean $HTTPOnly Optional. HTTPOnly. Default is false
639
     * @return void
640
     */
641 View Code Duplication
    public function setcookie(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
642
        $name,
643
        $value = '',
644
        $maxage = 0,
645
        $path = '',
646
        $domain = '',
647
        $secure = false,
648
        $HTTPOnly = false
649
    ) {
650
        $this->header(
651
            'Set-Cookie: ' . $name . '=' . rawurlencode($value)
652
            . (empty($domain) ? '' : '; Domain=' . $domain)
653
            . (empty($maxage) ? '' : '; Max-Age=' . $maxage)
654
            . (empty($path) ? '' : '; Path=' . $path)
655
            . (!$secure ? '' : '; Secure')
656
            . (!$HTTPOnly ? '' : '; HttpOnly'),
657
            false
658
        );
659
    }
660
661
    /**
662
     * Send the header
663
     * @param  string $s Header. Example: 'Location: http://php.net/'
664
     * @param  boolean $replace Optional. Replace?
665
     * @param  integer $code Optional. HTTP response code
0 ignored issues
show
Documentation introduced by
Should the type for parameter $code not be false|integer?

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...
666
     * @throws \PHPDaemon\Request\RequestHeadersAlreadySent
667
     * @return boolean Success
668
     */
669 View Code Duplication
    public function header($s, $replace = true, $code = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
670
    {
671
        if (!$code) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $code of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
672
            $this->status($code);
0 ignored issues
show
Bug introduced by
It seems like $code defined by parameter $code on line 669 can also be of type false; however, PHPDaemon\HTTPRequest\Generic::status() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
673
        }
674
675
        if ($this->headers_sent) {
676
            throw new RequestHeadersAlreadySent;
677
        }
678
        $s = strtr($s, "\r\n", '  ');
679
680
        $e = explode(':', $s, 2);
681
682
        if (!isset($e[1])) {
683
            $e[0] = 'STATUS';
684
685
            if (strncmp($s, 'HTTP/', 5) === 0) {
686
                $s = substr($s, 9);
687
            }
688
        }
689
690
        $k = strtr(strtoupper($e[0]), Generic::$htr);
691
692
        if ($k === 'CONTENT_TYPE') {
693
            self::parseStr(strtolower($e[1]), $ctype, true);
694
            if (!isset($ctype['charset'])) {
695
                $ctype['charset'] = $this->upstream->pool->config->defaultcharset->value;
696
697
                $s = $e[0] . ': ';
698
                $i = 0;
699
                foreach ($ctype as $k => $v) {
700
                    $s .= ($i > 0 ? '; ' : '') . $k . ($v !== '' ? '=' . $v : '');
701
                    ++$i;
702
                }
703
            }
704
        }
705
706
        if ($k === 'SET_COOKIE') {
707
            $k .= '_' . ++$this->cookieNum;
708
        } elseif (!$replace && isset($this->headers[$k])) {
709
            return false;
710
        }
711
712
        $this->headers[$k] = $s;
713
714
        if ($k === 'CONTENT_LENGTH') {
715
            $this->contentLength = (int)$e[1];
716
        } elseif ($k === 'LOCATION') {
717
            $this->status(301);
718
        }
719
720
        if (Daemon::$compatMode) {
721
            is_callable('header_native') ? header_native($s) : header($s);
722
        }
723
724
        return true;
725
    }
726
727
    /**
728
     * Removes a header
729
     * @param  string $s Header name. Example: 'Location'
730
     * @return void
731
     */
732
    public function removeHeader($s)
733
    {
734
        unset($this->headers[strtr(strtoupper($s), Generic::$htr)]);
735
    }
736
737
    /**
738
     * Converts human-readable representation of size to number of bytes
739
     * @param  string $value String of size
740
     * @return integer
741
     */
742 View Code Duplication
    public static function parseSize($value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
743
    {
744
        $l = substr($value, -1);
745
746
        if ($l === 'b' || $l === 'B') {
747
            return ((int)substr($value, 0, -1));
748
        }
749
750
        if ($l === 'k') {
751
            return ((int)substr($value, 0, -1) * 1000);
752
        }
753
754
        if ($l === 'K') {
755
            return ((int)substr($value, 0, -1) * 1024);
756
        }
757
758
        if ($l === 'm') {
759
            return ((int)substr($value, 0, -1) * 1000 * 1000);
760
        }
761
762
        if ($l === 'M') {
763
            return ((int)substr($value, 0, -1) * 1024 * 1024);
764
        }
765
766
        if ($l === 'g') {
767
            return ((int)substr($value, 0, -1) * 1000 * 1000 * 1000);
768
        }
769
770
        if ($l === 'G') {
771
            return ((int)substr($value, 0, -1) * 1024 * 1024 * 1024);
772
        }
773
774
        return (int)$value;
775
    }
776
777
    /**
778
     * Called when file upload started
779
     * @param  Input $in Input buffer
780
     * @return void
781
     */
782
    public function onUploadFileStart($in)
783
    {
784
        $this->freezeInput();
785
        FileSystem::tempnam(ini_get('upload_tmp_dir'), 'php', function ($fp) use ($in) {
786
            if (!$fp) {
787
                $in->curPart['fp'] = false;
788
                $in->curPart['error'] = UPLOAD_ERR_NO_TMP_DIR;
789
            } else {
790
                $in->curPart['fp'] = $fp;
791
                $in->curPart['tmp_name'] = $fp->path;
792
            }
793
            $this->unfreezeInput();
794
        });
795
    }
796
797
    /**
798
     * Called when chunk of incoming file has arrived
799
     * @param  Input $in Input buffer
800
     * @param  boolean $last Last?
801
     * @return void
802
     */
803
    public function onUploadFileChunk($in, $last = false)
804
    {
805
        if ($in->curPart['error'] !== UPLOAD_ERR_OK) {
806
            // just drop the chunk
807
            return;
808
        }
809
        $cb = function ($fp, $result) use ($last, $in) {
0 ignored issues
show
Unused Code introduced by
The parameter $fp is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $result is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
810
            if ($last) {
811
                unset($in->curPart['fp']);
812
            }
813
            $this->unfreezeInput();
814
        };
815
        if ($in->writeChunkToFd($in->curPart['fp']->getFd())) {
816
            // We had written via internal method
817
            return;
818
        }
819
        // Internal method is not available, let's get chunk data into $chunk and then use File->write()
820
        $chunk = $in->getChunkString();
821
        if ($chunk === false) {
822
            return;
823
        }
824
        $this->freezeInput();
825
        $in->curPart['fp']->write($chunk, $cb);
826
    }
827
828
    /**
829
     * Freeze input
830
     * @return void
831
     */
832
    protected function freezeInput()
833
    {
834
        $this->upstream->freezeInput();
835
        $this->attrs->input->freeze();
836
    }
837
838
    /**
839
     * Unfreeze input
840
     * @return void
841
     */
842
    protected function unfreezeInput()
843
    {
844
        $this->upstream->unfreezeInput();
845
        if (isset($this->attrs->input)) {
846
            $this->attrs->input->unfreeze();
847
        }
848
    }
849
850
    /**
851
     * Returns path to directory of temporary upload files
852
     * @return string
853
     */
854
    public function getUploadTempDir()
855
    {
856
        if ($r = ini_get('upload_tmp_dir')) {
857
            return $r;
858
        }
859
        return sys_get_temp_dir();
860
    }
861
862
    /**
863
     * Tells whether the file was uploaded via HTTP POST
864
     * @param  string $path The filename being checked
865
     * @return boolean       Whether if this is uploaded file
866
     */
867
    public function isUploadedFile($path)
868
    {
869
        if (!$path) {
870
            return false;
871
        }
872
        if (mb_orig_strpos($path, $this->getUploadTempDir() . '/') !== 0) {
873
            return false;
874
        }
875
        foreach ($this->attrs->files as $file) {
876
            if ($file['tmp_name'] === $path) {
877
                return true;
878
            }
879
        }
880
        return false;
881
    }
882
883
    /**
884
     * Moves an uploaded file to a new location
885
     * @param  string $filename The filename of the uploaded file
886
     * @param  string $dest The destination of the moved file
887
     * @return boolean           Success
0 ignored issues
show
Documentation introduced by
Should the return type not be resource|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
888
     */
889
    public function moveUploadedFile($filename, $dest)
890
    {
891
        if (!$this->isUploadedFile($filename)) {
892
            return false;
893
        }
894
        return FileSystem::rename($filename, $dest);
895
    }
896
897
    /**
898
     * Read request body from the file given in REQUEST_BODY_FILE parameter
899
     * @return boolean Success
900
     */
901
    public function readBodyFile()
902
    {
903
        if (!isset($this->attrs->server['REQUEST_BODY_FILE'])) {
904
            return false;
905
        }
906
907
        FileSystem::readfileChunked(
908
            $this->attrs->server['REQUEST_BODY_FILE'],
909
            function ($file, $success) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $success is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
910
                $this->attrs->inputDone = true;
911
                if ($this->sleepTime === 0) {
912
                    $this->wakeup();
913
                }
914
            },
915
            function ($file, $chunk) {
916
                // readed chunk
917
                $this->stdin($chunk);
0 ignored issues
show
Documentation Bug introduced by
The method stdin does not exist on object<PHPDaemon\HTTPRequest\Generic>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
918
            }
919
        );
920
921
        return true;
922
    }
923
924
    /**
925
     * Replacement for default parse_str(), it supoorts UCS-2 like this: %uXXXX
926
     * @param  string $s String to parse
927
     * @param  array &$var Reference to the resulting array
928
     * @param  boolean $header Header-style string
929
     * @return void
930
     */
931
    public static function parseStr($s, &$var, $header = false)
932
    {
933
        static $cb;
934
        if ($cb === null) {
935
            $cb = function ($m) {
936
                return urlencode(html_entity_decode('&#' . hexdec($m[1]) . ';', ENT_NOQUOTES, 'utf-8'));
937
            };
938
        }
939
        if ($header) {
940
            $s = strtr($s, Generic::$hvaltr);
941
        }
942
        if ((stripos($s, '%u') !== false)
943
            && preg_match('~(%u[a-f\d]{4}|%[c-f][a-f\d](?!%[89a-f][a-f\d]))~is', $s, $m)
944
        ) {
945
            $s = preg_replace_callback('~%(u[a-f\d]{4}|[a-f\d]{2})~i', $cb, $s);
946
        }
947
        parse_str($s, $var);
948
    }
949
950
    /**
951
     * Called after request finish
952
     * @param  callable $cb Callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|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...
953
     * @return void
954
     */
955
    protected function postFinishHandler($cb = null)
956
    {
957
        if (!$this->headers_sent) {
958
            $this->out('');
959
        }
960
        $this->sendfp = null;
961
        if (isset($this->attrs->files)) {
962
            foreach ($this->attrs->files as $f) {
963
                if (isset($f['tmp_name'])) {
964
                    FileSystem::unlink($f['tmp_name']);
965
                }
966
            }
967
        }
968
        if (isset($this->attrs->session)) {
969
            $this->sessionCommit($cb);
970
        } else {
971
            $cb === null || $cb();
972
        }
973
    }
974
}
975