Completed
Push — master ( f974f3...a3cf5c )
by Ivan
01:45
created

Request::fixedQueryParams()   D

Complexity

Conditions 9
Paths 66

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9.081

Importance

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