Completed
Push — master ( 047ed3...54fd6d )
by Ivan
01:36
created

Request::fixedQueryParams()   B

Complexity

Conditions 10
Paths 98

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10.6008

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 18
cts 22
cp 0.8182
rs 7.6666
c 0
b 0
f 0
cc 10
nc 98
nop 1
crap 10.6008

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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