Completed
Push — master ( 32f17b...085afb )
by Ivan
02:44
created

Request::marshalProtocolVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 8
cp 0
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 12
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
34
        if (null === $cookies && array_key_exists('cookie', $headers)) {
35
            $cookies = self::parseCookieHeader($headers['cookie']);
36
        }
37
        $uri = \Zend\Diactoros\marshalUriFromSapi($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'),
0 ignored issues
show
Deprecated Code introduced by
The method Zend\Diactoros\ServerRequestFactory::get() has been deprecated with message: since 1.8.0; no longer used internally.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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
            \Zend\Diactoros\marshalProtocolVersionFromSapi($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
            \Zend\Diactoros\normalizeUploadedFiles($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 1
    public function __construct(
229
        array $serverParams = [],
230
        array $uploadedFiles = [],
231
        $uri = null,
232
        $method = null,
233
        $body = 'php://input',
234
        array $headers = [],
235
        array $cookies = [],
236
        array $queryParams = [],
237
        $parsedBody = null,
238
        $protocol = '1.1'
239
    ) {
240 1
        $uri = new Uri((string)$uri);
241 1
        parent::__construct(
242 1
            $serverParams,
243 1
            $uploadedFiles,
244 1
            $uri,
245 1
            $method,
246 1
            $body,
247 1
            $headers,
248 1
            $cookies,
249 1
            $queryParams,
250 1
            $parsedBody,
251 1
            $protocol
252
        );
253 1
    }
254 1
    protected function cleanValue($value, $mode = null)
255
    {
256 1
        if (is_array($value)) {
257
            $temp = [];
258
            foreach ($value as $k => $v) {
259
                $temp[$k] = $this->cleanValue($v, $mode);
260
            }
261
            return $temp;
262
        }
263
        // normalize newlines
264 1
        if (strpos((string)$value, "\r") !== false) {
265
            $value = str_replace(array("\r\n", "\r", "\r\n\n"), PHP_EOL, $value);
266
        }
267
        // remove invalid utf8 chars
268 1
        if (preg_match('/[^\x00-\x7F]/S', $value) != 0) {
269
            $temp = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
270
            if ($temp !== false) {
271
                $value = $temp;
272
            }
273
        }
274
        // remove non-printable chars
275
        do {
276 1
            $count = 0;
277 1
            $value = preg_replace(['/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'], '', $value, -1, $count);
278 1
        } while ((int)$count > 0);
279
280
        switch ($mode) {
281 1
            case 'int':
282 1
                $value = (int) $value;
283 1
                break;
284 1
            case 'float':
285
                $value = (float) $value;
286
                break;
287 1
            case 'nohtml':
288
                $value = strip_tags((string) $value);
289
                break;
290 1
            case 'escape':
291
                $value = htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE);
292
                break;
293 1
            case 'string':
294
                $value = (string) $value;
295
                break;
296 1
            case 'raw':
297
            default:
298 1
                break;
299
        }
300
301 1
        return $value;
302
    }
303 1
    protected function getValue(array $collection, $key, $default, $mode)
304
    {
305 1
        if ($key === null) {
306
            return $this->cleanValue($collection, $mode);
307
        }
308 1
        return isset($collection[$key]) ? $this->cleanValue($collection[$key], $mode) : $default;
309
    }
310
    /**
311
     * Gets a value from a cookie that came with the request
312
     * @param  string    $key     the cookie name
313
     * @param  mixed     $default optional default value to return if the key is not present (default to `null`)
314
     * @param  string    $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
315
     * @return mixed             the value (or values)
316
     */
317 1
    public function getCookie($key = null, $default = null, $mode = null)
318
    {
319 1
        return $this->getValue($this->getCookieParams(), $key, $default, $mode);
320
    }
321
    /**
322
     * Get a GET param from the request URL
323
     * @param  string   $key     the GET param name
324
     * @param  mixed    $default optional default value to return if the key is not present (default to `null`)
325
     * @param  string   $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
326
     * @return mixed             the value (or values)
327
     */
328 1
    public function getQuery($key = null, $default = null, $mode = null)
329
    {
330 1
        return $this->getValue($this->getQueryParams(), $key, $default, $mode);
331
    }
332
    /**
333
     * Get a param from the request body (if it is in JSON format it will be parsed out as well)
334
     * @param  string   $key     the param name
335
     * @param  mixed    $default optional default value to return if the key is not present (default to `null`)
336
     * @param  string   $mode    optional cleanup of the value, available modes are: int, float, nohtml, escape, string
337
     * @return mixed             the value (or values if no key was specified)
338
     */
339
    public function getPost($key = null, $default = null, $mode = null)
340
    {
341
        $body = $this->getParsedBody();
342
        if (!is_array($body)) {
343
            $body = [];
344
        }
345
        return $this->getValue($body, $key, $default, $mode);
346
    }
347
    /**
348
     * Get any authorization details supplied with the request.
349
     * @return array|null           array of extracted values or null (possible keys are username, password and token)
350
     */
351
    public function getAuthorization()
352
    {
353
        if (!$this->hasHeader('Authorization')) {
354
            return null;
355
        }
356
        $temp = explode(' ', trim($this->getHeaderLine('Authorization')), 2);
357
        switch (strtolower($temp[0])) {
358
            case 'basic':
359
                $temp[1] = base64_decode($temp[1]);
360
                $temp[1] = explode(':', $temp[1], 2);
361
                return ['username' => $temp[1][0], 'password' => $temp[1][1] ?? null];
362
            case 'token':
363
            case 'oauth':
364
            case 'bearer':
365
                return ['token' => $temp[1] ?? null];
366
            default:
367
                return null;
368
        }
369
    }
370
    public function getUrl()
371
    {
372
        return $this->getUri();
373
    }
374
    /**
375
     * Determine if this is an AJAX request
376
     * @return boolean is the request AJAX
377
     */
378 1
    public function isAjax()
379
    {
380 1
        return ($this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest');
381
    }
382
    /**
383
     * Determine if this is an CORS request
384
     * @return boolean is the request CORS
385
     */
386 1
    public function isCors()
387
    {
388 1
        if (!$this->hasHeader('Origin')) {
389 1
            return false;
390
        }
391
        $origin = parse_url($this->getHeaderLine('Origin'));
392
        $host   = $this->getUri()->getHost();
393
        $scheme = $this->getUri()->getScheme();
394
        return (
395
            !$host ||
396
            strtolower($origin['scheme']?? '') !== strtolower($scheme) ||
397
            strpos(strtolower($origin['host'] ?? ''), strtolower($host)) === false
398
        );
399
    }
400
    /**
401
     * Get the prefered response languages (parses the Accept-Language header if present).
402
     * @param  bool    $shortNames should values like "en-US", be truncated to "en", defaults to true
403
     * @return array   array of ordered lowercase language codes
404
     */
405 1
    public function getPreferredResponseLanguages(bool $shortNames = true) : array
406
    {
407 1
        $acpt = $this->getHeaderLine('Accept-Language') ?: '*';
408 1
        $acpt = explode(',', $acpt);
409 1
        foreach ($acpt as $k => $v) {
410 1
            $v = array_pad(explode(';', $v, 2), 2, 'q=1');
411 1
            $v[1] = (float) array_pad(explode('q=', $v[1], 2), 2, '1')[1];
412 1
            $v[0] = $shortNames ? explode('-', $v[0], 2)[0] : $v[0];
413 1
            $v[2] = $k;
414 1
            $acpt[$k] = $v;
415
        }
416
        usort($acpt, function ($a, $b) {
417 1
            if ($a[1] > $b[1]) {
418 1
                return -1;
419
            }
420
            if ($a[1] < $b[1]) {
421
                return 1;
422
            }
423
            return $a[2] < $b[2] ? -1 : 1;
424 1
        });
425
        $acpt = array_map(function ($v) {
426 1
            return strtolower($v[0]);
427 1
        }, $acpt);
428
        $acpt = array_filter($acpt, function ($v) {
429 1
            return $v !== '*';
430 1
        });
431 1
        return array_unique($acpt);
432
    }
433
    /**
434
     * Get the preffered response language (parses the Accept-Language header if present).
435
     * @param  string       $default the default code to return if the header is not found
436
     * @param  array|null   $allowed an optional list of lowercase language codes to intersect with, defaults to null
437
     * @return string       the prefered language code
438
     */
439 1
    public function getPreferredResponseLanguage(string $default = 'en', array $allowed = null) : string
440
    {
441 1
        $acpt = $this->getPreferredResponseLanguages(true);
442 1
        foreach ($acpt as $lang) {
443 1
            if ($allowed === null) {
444 1
                return $lang;
445
            }
446
            if (in_array($lang, $allowed)) {
447
                return $lang;
448
            }
449
        }
450 1
        return $default;
451
    }
452
    /**
453
     * Get the prefered response formats.
454
     * @param  string                    $default the default value to return if the Accept header is not present.
455
     * @return string[]                  the desired response formats
456
     */
457 1
    public function getPreferredResponseFormats($default = 'text/html')
458
    {
459
        // parse accept header (uses default instead of 406 header)
460 1
        $acpt = $this->getHeaderLine('Accept') ?: $default;
461 1
        $acpt = explode(',', $acpt);
462 1
        foreach ($acpt as $k => $v) {
463 1
            $v = array_pad(explode(';', $v, 2), 2, 'q=1');
464 1
            $v[1] = (float) array_pad(explode('q=', $v[1], 2), 2, '1')[1];
465 1
            $v[0] = $v[0];
466 1
            $v[2] = $k;
467 1
            $acpt[$k] = $v;
468
        }
469
        usort($acpt, function ($a, $b) {
470 1
            if ($a[1] > $b[1]) {
471 1
                return -1;
472
            }
473 1
            if ($a[1] < $b[1]) {
474
                return 1;
475
            }
476 1
            return $a[2] < $b[2] ? -1 : 1;
477 1
        });
478
        $acpt = array_map(function ($v) {
479 1
            return strtolower($v[0]);
480 1
        }, $acpt);
481 1
        $acpt = array_filter($acpt, function ($v) {
482 1
            return $v !== '*/*';
483 1
        });
484 1
        return array_unique($acpt);
485
    }
486
    /**
487
     * Get the preffered response language (parses the Accept-Language header if present).
488
     * @param  string       $default the default code to return if the header is not found
489
     * @param  array|null   $allowed an optional list of lowercase language codes to intersect with, defaults to null
490
     * @return string       the prefered language code
491
     */
492 1
    public function getPreferredResponseFormat(string $default = 'text/html', array $allowed = null) : string
493
    {
494
        // parse accept header (uses default instead of 406 header)
495 1
        $acpt = $this->getPreferredResponseFormats();
496 1
        foreach ($acpt as $format) {
497 1
            if ($allowed === null) {
498 1
                return $format;
499
            }
500
            if (in_array($format, $allowed)) {
501
                return $format;
502
            }
503
        }
504
        return $default;
505
    }
506
    /**
507
     * Retrieves the URI instance.
508
     * 
509
     * @return \vakata\http\Uri Returns a Uri instance
510
     */
511
    public function getUri()
512
    {
513
        return parent::getUri();
514
    }
515
}
516