Request   F
last analyzed

Complexity

Total Complexity 119

Size/Duplication

Total Lines 553
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 6

Test Coverage

Coverage 54.14%

Importance

Changes 0
Metric Value
wmc 119
lcom 4
cbo 6
dl 0
loc 553
ccs 170
cts 314
cp 0.5414
rs 2
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
D fromGlobals() 0 63 23
F fromString() 0 113 23
C fixedQueryParams() 0 35 11
A parseCookieHeader() 0 22 3
A __construct() 0 30 2
C cleanValue() 0 49 13
A getValue() 0 7 3
A getCookie() 0 4 1
A getQuery() 0 4 1
A getPost() 0 8 2
A getAuthorization() 0 19 6
A getUrl() 0 4 1
A isAjax() 0 4 1
A isCors() 0 14 4
B getPreferredResponseLanguages() 0 28 7
A getPreferredResponseLanguage() 0 13 4
B getPreferredResponseFormats() 0 29 6
A getPreferredResponseFormat() 0 14 4
A hasCertificate() 0 4 1
A getCertificateNumber() 0 4 1
A getCertificate() 0 4 1
A withCertificate() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like Request 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 Request, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace vakata\http;
4
5
use Laminas\Diactoros\Uri as LaminasUri;
6
use Laminas\Diactoros\Stream;
7
use Laminas\Diactoros\UploadedFile;
8
use Laminas\Diactoros\ServerRequest;
9
use Laminas\Diactoros\ServerRequestFactory;
10
11
class Request extends ServerRequest
12
{
13
    protected $certificateNumber;
14
    protected $certificateData;
15
    /**
16
     * Create an instance from globals
17
     *
18
     * @param array $server
19
     * @param array $query
20
     * @param array $body
21
     * @param array $cookies
22
     * @param array $files
23
     * @return Request
24
     */
25
    public static function fromGlobals(
26
        array $server = null,
27
        array $query = null,
28
        array $body = null,
29
        array $cookies = null,
30
        array $files = null
31
    ) {
32
        $server  = \Laminas\Diactoros\normalizeServer($server ?: $_SERVER);
33
        $files   = \Laminas\Diactoros\normalizeUploadedFiles($files ?: $_FILES);
34
        $headers = [];
35
        foreach ($server as $key => $value) {
36
            if (strpos($key, 'REDIRECT_') === 0) {
37
                $key = substr($key, 9);
38
                if (array_key_exists($key, $server)) {
39
                    continue;
40
                }
41
            }
42
            if (is_string($value) && strlen($value) && strpos($key, 'HTTP_') === 0) {
43
                $name = strtr(strtolower(substr($key, 5)), '_', '-');
44
                $headers[$name] = $value;
45
                continue;
46
            }
47
            if (is_string($value) && strlen($value) && strpos($key, 'CONTENT_') === 0) {
48
                $name = 'content-' . strtolower(substr($key, 8));
49
                $headers[$name] = $value;
50
                continue;
51
            }
52
        }
53
54
        $method  = \Laminas\Diactoros\marshalMethodFromSapi($server);
55
        $uri     = \Laminas\Diactoros\marshalUriFromSapi($server, $headers);
56
57
        if (null === $cookies && array_key_exists('cookie', $headers)) {
58
            $cookies = self::parseCookieHeader($headers['cookie']);
59
        }
60
        
61
62
        if ($body === null) {
63
            $temp = file_get_contents('php://input');
64
            if ($temp !== false && strlen($temp)) {
65
                if (isset($headers['content-type']) && strpos($headers['content-type'], 'json') !== false) {
66
                    $body = json_decode($temp, true);
67
                } else {
68
                    $body = static::fixedQueryParams($temp);
69
                }
70
            }
71
        }
72
73
        return new static(
74
            $server,
75
            $files,
76
            $uri,
77
            $method,
78
            'php://input',
79
            $headers,
80
            $cookies ?: $_COOKIE,
81
            $query ?: static::fixedQueryParams($uri->getQuery()),
82
            $body ?: (count($_POST) ? $_POST : null),
83
            \Laminas\Diactoros\marshalProtocolVersionFromSapi($server),
84
            $server['SSL_CLIENT_M_SERIAL'] ?? null,
85
            $server['SSL_CLIENT_CERT'] ?? null
86
        );
87
    }
88 1
    public static function fromString(string $str) : Request
89
    {
90 1
        $method = 'GET';
91 1
        $version = '1.1';
92 1
        $uri = '/';
93 1
        $headers = [];
94 1
        $files = [];
95 1
        $body = '';
96
97 1
        $break = strpos($str, "\r\n\r\n") === false ? "\n" : "\r\n"; // just in case someone breaks RFC 2616
98
99 1
        list($headers, $message) = array_pad(explode($break . $break, $str, 2), 2, '');
100 1
        $headers = explode($break, preg_replace("(" . $break . "\s+)", " ", $headers));
101 1
        if (isset($headers[0]) && strlen($headers[0])) {
102 1
            $temp = explode(' ', $headers[0]);
103 1
            if (in_array($temp[0], ['GET', 'POST', 'HEAD', 'PATCH', 'PUT', 'OPTIONS', 'TRACE', 'DELETE'])) {
104 1
                $method = $temp[0];
105 1
                $uri = $temp[1];
106 1
                if (isset($temp[2])) {
107 1
                    $version = substr($temp[2], 5);
108
                }
109 1
                unset($headers[0]);
110 1
                $headers = array_values($headers);
111
            }
112
        }
113 1
        $temp = array_filter($headers);
114 1
        $headers = [];
115 1
        foreach ($temp as $v) {
116 1
            $v = explode(':', $v, 2);
117 1
            $name = trim($v[0]);
118 1
            $name = str_replace('_', ' ', strtolower($name));
119 1
            $name = str_replace('-', ' ', strtolower($name));
120 1
            $name = str_replace(' ', '-', ucwords($name));
121 1
            $headers[$name] = trim($v[1]);
122
        }
123 1
        if (isset($headers['Host'])) {
124
            $uri = $headers['Host'] . $uri;
125
        } else {
126 1
            $uri = 'localhost' . $uri;
127
        }
128 1
        if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart') !== false) {
129
            $bndr = trim(explode(' boundary=', $headers['Content-Type'])[1], '"');
130
            $parts = explode($break . '--' . $bndr, $break . $message);
131
            if (count($parts) == 1) {
132
                $body = $message;
133
            } else {
134
                array_pop($parts);
135
                array_shift($parts);
136
                $post = [];
137
                $fres = [];
138
                foreach ($parts as $k => $item) {
139
                    list($head, $pbody) = explode($break . $break, $item, 2);
140
                    $head = explode($break, preg_replace("(" . $break . "\s+)", " ", $head));
141
                    foreach ($head as $h) {
142
                        if (strpos(strtolower($h), 'content-disposition') === 0) {
143
                            $cd = explode(';', $h);
144
                            $name = '';
145
                            $file = '';
146
                            foreach ($cd as $p) {
147
                                if (strpos(trim($p), 'name=') === 0) {
148
                                    $name = trim(explode('name=', $p)[1], ' "');
149
                                }
150
                                if (strpos(trim($p), 'filename=') === 0) {
151
                                    $file = trim(explode('filename=', $p)[1], ' "');
152
                                }
153
                            }
154
                            if ($file) {
155
                                // create resource manually
156
                                $fres[$k] = fopen('php://temp', 'wb+');
157
                                fwrite($fres[$k], $pbody);
158
                                rewind($fres[$k]);
159
                                $files[$name] = new UploadedFile(
160
                                    $fres[$k],
161
                                    strlen($pbody),
162
                                    UPLOAD_ERR_OK,
163
                                    $file
164
                                );
165
                            } else {
166
                                $post[$name] = $pbody;
167
                            }
168
                        }
169
                    }
170
                }
171
                $body = http_build_query($post);
172
            }
173 1
        } elseif (strlen($message)) {
174
            $body = $message;
175
        }
176 1
        if (strpos($uri, '://') === false) {
177 1
            $uri = 'http://' . $uri;
178
        }
179
180 1
        if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'json') !== false) {
181
            $params = json_decode($body, true);
182
        } else {
183 1
            $params = static::fixedQueryParams($body);
184
        }
185 1
        $temp = (new Stream('php://temp', 'wb+'));
186 1
        $temp->write($body);
187 1
        $uri = new LaminasUri($uri);
188 1
        return new static(
189 1
            [],
190 1
            \Laminas\Diactoros\normalizeUploadedFiles($files),
191 1
            $uri,
192 1
            $method,
193 1
            $temp,
194 1
            $headers,
195 1
            isset($headers['Cookie']) ? self::parseCookieHeader($headers['Cookie']) : [],
196 1
            static::fixedQueryParams($uri->getQuery()),
197 1
            $params ?? [],
198 1
            $version
199
        );
200
    }
201 2
    public static function fixedQueryParams($query)
202
    {
203 2
        $data = [];
204 2
        $temp = strlen($query) ? explode('&', $query) : [];
205 2
        foreach ($temp as $var) {
206 2
            $var   = explode('=', $var, 2);
207 2
            $name  = urldecode($var[0]);
208 2
            $value = isset($var[1]) ? urldecode($var[1]) : '';
209 2
            $name  = explode(']', str_replace(['][', '['], ']', $name));
210 2
            $name  = count($name) > 1 ? array_slice($name, 0, -1) : $name;
211
212 2
            $tmp = &$data;
213 2
            foreach ($name as $k) {
214 2
                if ($k === "") {
215
                    continue;
216
                }
217 2
                if (!is_array($tmp)) {
218
                    $tmp = [];
219
                }
220 2
                if (!isset($tmp[$k])) {
221 2
                    $tmp[$k] = [];
222
                }
223 2
                $tmp = &$tmp[$k];
224
            }
225 2
            if ($name[count($name) - 1] == '') {
226
                if (!is_array($tmp)) {
227
                    $tmp = [];
228
                }
229
                $tmp[] = $value;
230
            } else {
231 2
                $tmp = $value;
232
            }
233
        }
234 2
        return $data;
235
    }
236 1
    private static function parseCookieHeader($cookieHeader)
237
    {
238 1
        preg_match_all('(
239
            (?:^\\n?[ \t]*|;[ ])
240
            (?P<name>[!#$%&\'*+-.0-9A-Z^_`a-z|~]+)
241
            =
242
            (?P<DQUOTE>"?)
243
                (?P<value>[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*)
244
            (?P=DQUOTE)
245
            (?=\\n?[ \t]*$|;[ ])
246 1
        )x', $cookieHeader, $matches, PREG_SET_ORDER);
247
248 1
        $cookies = [];
249
250 1
        if (is_array($matches)) {
251 1
            foreach ($matches as $match) {
252 1
                $cookies[$match['name']] = urldecode($match['value']);
253
            }
254
        }
255
256 1
        return $cookies;
257
    }
258 1
    public function __construct(
259
        array $serverParams = [],
260
        array $uploadedFiles = [],
261
        $uri = null,
262
        $method = null,
263
        $body = 'php://input',
264
        array $headers = [],
265
        array $cookies = [],
266
        array $queryParams = [],
267
        $parsedBody = null,
268
        $protocol = '1.1',
269
        string $certificateNumber = null,
270
        string $certificateData = null
271
    ) {
272 1
        $uri = new Uri((string)$uri);
273 1
        parent::__construct(
274 1
            $serverParams,
275 1
            $uploadedFiles,
276 1
            $uri,
277 1
            $method,
278 1
            $body,
279 1
            $headers,
280 1
            $cookies,
281 1
            $queryParams,
282 1
            $parsedBody,
283 1
            $protocol
284
        );
285 1
        $this->certificateNumber = $certificateNumber ? strtoupper(ltrim(trim($certificateNumber), '0')) : null;
286 1
        $this->certificateData = $certificateData;
287 1
    }
288 1
    protected function cleanValue($value, $mode = null)
289
    {
290 1
        if (is_array($value)) {
291
            $temp = [];
292
            foreach ($value as $k => $v) {
293
                $temp[$k] = $this->cleanValue($v, $mode);
294
            }
295
            return $temp;
296
        }
297
        // normalize newlines
298 1
        if (strpos((string)$value, "\r") !== false) {
299
            $value = str_replace(array("\r\n", "\r", "\r\n\n"), PHP_EOL, $value);
300
        }
301
        // remove invalid utf8 chars
302 1
        if (preg_match('/[^\x00-\x7F]/S', $value) != 0) {
303
            $temp = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
304
            if ($temp !== false) {
305
                $value = $temp;
306
            }
307
        }
308
        // remove non-printable chars
309
        do {
310 1
            $count = 0;
311 1
            $value = preg_replace(['/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'], '', $value, -1, $count);
312 1
        } while ((int)$count > 0);
313
314
        switch ($mode) {
315 1
            case 'int':
316 1
                $value = (int) $value;
317 1
                break;
318 1
            case 'float':
319
                $value = (float) $value;
320
                break;
321 1
            case 'nohtml':
322
                $value = strip_tags((string) $value);
323
                break;
324 1
            case 'escape':
325
                $value = htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE);
326
                break;
327 1
            case 'string':
328
                $value = (string) $value;
329
                break;
330 1
            case 'raw':
331
            default:
332 1
                break;
333
        }
334
335 1
        return $value;
336
    }
337 1
    protected function getValue(array $collection, $key, $default, $mode)
338
    {
339 1
        if ($key === null) {
340
            return $this->cleanValue($collection, $mode);
341
        }
342 1
        return isset($collection[$key]) ? $this->cleanValue($collection[$key], $mode) : $default;
343
    }
344
    /**
345
     * Gets a value from a cookie that came with the request
346
     * @param  string    $key     the cookie name
347
     * @param  mixed     $default optional default value to return if the key is not present (default to `null`)
348
     * @param  string    $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
349
     * @return mixed             the value (or values)
350
     */
351 1
    public function getCookie($key = null, $default = null, $mode = null)
352
    {
353 1
        return $this->getValue($this->getCookieParams(), $key, $default, $mode);
354
    }
355
    /**
356
     * Get a GET param from the request URL
357
     * @param  string   $key     the GET param name
358
     * @param  mixed    $default optional default value to return if the key is not present (default to `null`)
359
     * @param  string   $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
360
     * @return mixed             the value (or values)
361
     */
362 1
    public function getQuery($key = null, $default = null, $mode = null)
363
    {
364 1
        return $this->getValue($this->getQueryParams(), $key, $default, $mode);
365
    }
366
    /**
367
     * Get a param from the request body (if it is in JSON format it will be parsed out as well)
368
     * @param  string   $key     the param name
369
     * @param  mixed    $default optional default value to return if the key is not present (default to `null`)
370
     * @param  string   $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
371
     * @return mixed             the value (or values if no key was specified)
372
     */
373
    public function getPost($key = null, $default = null, $mode = null)
374
    {
375
        $body = $this->getParsedBody();
376
        if (!is_array($body)) {
377
            $body = [];
378
        }
379
        return $this->getValue($body, $key, $default, $mode);
380
    }
381
    /**
382
     * Get any authorization details supplied with the request.
383
     * @return array|null           array of extracted values or null (possible keys are username, password and token)
384
     */
385
    public function getAuthorization()
386
    {
387
        if (!$this->hasHeader('Authorization')) {
388
            return null;
389
        }
390
        $temp = explode(' ', trim($this->getHeaderLine('Authorization')), 2);
391
        switch (strtolower($temp[0])) {
392
            case 'basic':
393
                $temp[1] = base64_decode($temp[1]);
394
                $temp[1] = explode(':', $temp[1], 2);
395
                return ['username' => $temp[1][0], 'password' => $temp[1][1] ?? null];
396
            case 'token':
397
            case 'oauth':
398
            case 'bearer':
399
                return ['token' => $temp[1] ?? null];
400
            default:
401
                return null;
402
        }
403
    }
404
    /**
405
     * Get the Uri object
406
     * @return Uri
407
     */
408
    public function getUrl()
409
    {
410
        return $this->getUri();
411
    }
412
    /**
413
     * Determine if this is an AJAX request
414
     * @return boolean is the request AJAX
415
     */
416 1
    public function isAjax()
417
    {
418 1
        return ($this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest');
419
    }
420
    /**
421
     * Determine if this is an CORS request
422
     * @return boolean is the request CORS
423
     */
424 1
    public function isCors()
425
    {
426 1
        if (!$this->hasHeader('Origin')) {
427 1
            return false;
428
        }
429
        $origin = parse_url($this->getHeaderLine('Origin'));
430
        $host   = $this->getUri()->getHost();
431
        $scheme = $this->getUri()->getScheme();
432
        return (
433
            !$host ||
434
            strtolower($origin['scheme']?? '') !== strtolower($scheme) ||
435
            strpos(strtolower($origin['host'] ?? ''), strtolower($host)) === false
436
        );
437
    }
438
    /**
439
     * Get the prefered response languages (parses the Accept-Language header if present).
440
     * @param  bool    $shortNames should values like "en-US", be truncated to "en", defaults to true
441
     * @return array   array of ordered lowercase language codes
442
     */
443 1
    public function getPreferredResponseLanguages(bool $shortNames = true) : array
444
    {
445 1
        $acpt = $this->getHeaderLine('Accept-Language') ?: '*';
446 1
        $acpt = explode(',', $acpt);
447 1
        foreach ($acpt as $k => $v) {
448 1
            $v = array_pad(explode(';', $v, 2), 2, 'q=1');
449 1
            $v[1] = (float) array_pad(explode('q=', $v[1], 2), 2, '1')[1];
450 1
            $v[0] = $shortNames ? explode('-', $v[0], 2)[0] : $v[0];
451 1
            $v[2] = $k;
452 1
            $acpt[$k] = $v;
453
        }
454
        usort($acpt, function ($a, $b) {
455 1
            if ($a[1] > $b[1]) {
456 1
                return -1;
457
            }
458
            if ($a[1] < $b[1]) {
459
                return 1;
460
            }
461
            return $a[2] < $b[2] ? -1 : 1;
462 1
        });
463
        $acpt = array_map(function ($v) {
464 1
            return strtolower($v[0]);
465 1
        }, $acpt);
466
        $acpt = array_filter($acpt, function ($v) {
467 1
            return $v !== '*';
468 1
        });
469 1
        return array_unique($acpt);
470
    }
471
    /**
472
     * Get the preffered response language (parses the Accept-Language header if present).
473
     * @param  string       $default the default code to return if the header is not found
474
     * @param  array|null   $allowed an optional list of lowercase language codes to intersect with, defaults to null
475
     * @return string       the prefered language code
476
     */
477 1
    public function getPreferredResponseLanguage(string $default = 'en', array $allowed = null) : string
478
    {
479 1
        $acpt = $this->getPreferredResponseLanguages(true);
480 1
        foreach ($acpt as $lang) {
481 1
            if ($allowed === null) {
482 1
                return $lang;
483
            }
484
            if (in_array($lang, $allowed)) {
485
                return $lang;
486
            }
487
        }
488 1
        return $default;
489
    }
490
    /**
491
     * Get the prefered response formats.
492
     * @param  string                    $default the default value to return if the Accept header is not present.
493
     * @return string[]                  the desired response formats
494
     */
495 1
    public function getPreferredResponseFormats($default = 'text/html')
496
    {
497
        // parse accept header (uses default instead of 406 header)
498 1
        $acpt = $this->getHeaderLine('Accept') ?: $default;
499 1
        $acpt = explode(',', $acpt);
500 1
        foreach ($acpt as $k => $v) {
501 1
            $v = array_pad(explode(';', $v, 2), 2, 'q=1');
502 1
            $v[1] = (float) array_pad(explode('q=', $v[1], 2), 2, '1')[1];
503 1
            $v[0] = $v[0];
504 1
            $v[2] = $k;
505 1
            $acpt[$k] = $v;
506
        }
507
        usort($acpt, function ($a, $b) {
508 1
            if ($a[1] > $b[1]) {
509 1
                return -1;
510
            }
511 1
            if ($a[1] < $b[1]) {
512
                return 1;
513
            }
514 1
            return $a[2] < $b[2] ? -1 : 1;
515 1
        });
516
        $acpt = array_map(function ($v) {
517 1
            return strtolower($v[0]);
518 1
        }, $acpt);
519 1
        $acpt = array_filter($acpt, function ($v) {
520 1
            return $v !== '*/*';
521 1
        });
522 1
        return array_unique($acpt);
523
    }
524
    /**
525
     * Get the preffered response language (parses the Accept-Language header if present).
526
     * @param  string       $default the default code to return if the header is not found
527
     * @param  array|null   $allowed an optional list of lowercase language codes to intersect with, defaults to null
528
     * @return string       the prefered language code
529
     */
530 1
    public function getPreferredResponseFormat(string $default = 'text/html', array $allowed = null) : string
531
    {
532
        // parse accept header (uses default instead of 406 header)
533 1
        $acpt = $this->getPreferredResponseFormats();
534 1
        foreach ($acpt as $format) {
535 1
            if ($allowed === null) {
536 1
                return $format;
537
            }
538
            if (in_array($format, $allowed)) {
539
                return $format;
540
            }
541
        }
542
        return $default;
543
    }
544
    public function hasCertificate()
545
    {
546
        return $this->certificateNumber !== null;
547
    }
548
    public function getCertificateNumber()
549
    {
550
        return $this->certificateNumber;
551
    }
552
    public function getCertificate()
553
    {
554
        return $this->certificateData;
555
    }
556
    public function withCertificate(string $number, string $data = null)
557
    {
558
        $ret = clone $this;
559
        $ret->certificateNumber = strtoupper(ltrim(trim($number), '0'));
560
        $ret->certificateData = $data;
561
        return $ret;
562
    }
563
}
564