Completed
Push — master ( 503659...32f17b )
by Ivan
02:01
created

Request::getUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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 mixed             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 mixed             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 mixed             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
     * Retrieves the URI instance.
523
     * 
524
     * @return \vakata\http\Uri Returns a Uri instance
525
     */
526
    public function getUri()
527
    {
528
        return $this->uri;
0 ignored issues
show
Bug introduced by
The property uri cannot be accessed from this context as it is declared private in class Zend\Diactoros\RequestTrait.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
529
    }
530
}
531