Completed
Push — master ( 4c67b7...c424ac )
by Ivan
10:21 queued 08:01
created

Request::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 14
cts 14
cp 1
rs 9.504
c 0
b 0
f 0
cc 1
nc 1
nop 10
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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