Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.91% covered (warning)
89.91%
98 / 109
88.46% covered (warning)
88.46%
23 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpRequest
89.91% covered (warning)
89.91%
98 / 109
88.46% covered (warning)
88.46%
23 / 26
109.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 initRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 initCurrentRequest
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 initNonGetData
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
901.67
 setMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequestLanguage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getRequestCountry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLocale
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 cleanupGlobals
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createFromSuperglobals
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setUri
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createRequestHashs
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 isMobile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getRequestInfo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getBrowser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 setBrowser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOS
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 setOS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrigin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isHttps
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
7
 getBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getRouteVerb
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 getReferer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMethod
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 rest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Message\Http
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Message\Http;
16
17use phpOMS\Localization\ISO3166TwoEnum;
18use phpOMS\Localization\ISO639x1Enum;
19use phpOMS\Localization\Localization;
20use phpOMS\Message\RequestAbstract;
21use phpOMS\Router\RouteVerb;
22use phpOMS\Security\Guard;
23use phpOMS\Uri\HttpUri;
24use phpOMS\Uri\UriInterface;
25
26/**
27 * Request class.
28 *
29 * @package phpOMS\Message\Http
30 * @license OMS License 2.0
31 * @link    https://jingga.app
32 * @since   1.0.0
33 *
34 * @SuppressWarnings(PHPMD.Superglobals)
35 *
36 * @property HttpHeader $header
37 */
38final class HttpRequest extends RequestAbstract
39{
40    /**
41     * Request method.
42     *
43     * @var string
44     * @since 1.0.0
45     */
46    protected string $method;
47
48    /**
49     * Browser type.
50     *
51     * @var string
52     * @since 1.0.0
53     */
54    private string $browser;
55
56    /**
57     * OS type.
58     *
59     * @var string
60     * @since 1.0.0
61     */
62    private string $os;
63
64    /**
65     * Request information.
66     *
67     * @var array{browser:string, os:string}
68     * @since 1.0.0
69     */
70    private array $info;
71
72    /**
73     * Constructor.
74     *
75     * @param UriInterface $uri  Uri
76     * @param Localization $l11n Localization
77     *
78     * @since 1.0.0
79     */
80    public function __construct(UriInterface $uri = null, Localization $l11n = null)
81    {
82        $this->header       = new HttpHeader();
83        $this->header->l11n = $l11n ?? new Localization();
84        $this->uri          = $uri ?? new HttpUri('');
85    }
86
87    /**
88     * Init request.
89     *
90     * This is used in order to either initialize the current http request or a batch of GET requests
91     *
92     * @return void
93     *
94     * @since 1.0.0
95     */
96    public function initRequest() : void
97    {
98        $this->initCurrentRequest();
99        self::cleanupGlobals();
100
101        $this->data = \array_change_key_case($this->data, \CASE_LOWER);
102        $this->data = Guard::unslash($this->data);
103    }
104
105    /**
106     * Init current request
107     *
108     * @return void
109     *
110     * @since 1.0.0
111     */
112    private function initCurrentRequest() : void
113    {
114        $this->uri   = HttpUri::fromCurrent();
115        $this->data  = $_POST + $_GET;
116        $this->files = $_FILES;
117        $this->header->initCurrentRequest();
118        $this->header->l11n->setLanguage($this->getRequestLanguage());
119        $this->header->l11n->setCountry($this->getRequestCountry());
120
121        $this->initNonGetData();
122    }
123
124    /**
125     * Init non get data
126     *
127     * @return void
128     *
129     * @throws \Exception
130     *
131     * @since 1.0.0
132     */
133    private function initNonGetData() : void
134    {
135        if (!isset($_SERVER['CONTENT_TYPE'])) {
136            return;
137        }
138
139        if (\stripos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
140            // @codeCoverageIgnoreStart
141            // Tested but coverage doesn't show up
142            $stream = \fopen('php://input', 'r');
143            if ($stream === false) {
144                return;
145            }
146
147            $input = '';
148            $size  = 0;
149
150            while (($lineRaw = \fgets($stream, 1024)) !== false) {
151                // Limit json data to 1MB
152                if ($size > 1000000) {
153                    \fclose($stream);
154
155                    return;
156                }
157
158                $input .= $lineRaw;
159                $size  += \strlen($lineRaw);
160            }
161
162            \fclose($stream);
163
164            if (empty($input)) {
165                return;
166            }
167
168            $json = \json_decode($input, true);
169            if (!\is_array($json)) {
170                throw new \Exception('Is not valid json ' . $input);
171            }
172
173            $this->data = $json + $this->data;
174            // @codeCoverageIgnoreEnd
175        } elseif (\stripos($_SERVER['CONTENT_TYPE'], 'application/x-www-form-urlencoded') !== false) {
176            // @codeCoverageIgnoreStart
177            // Tested but coverage doesn't show up
178            $stream = \fopen('php://input', 'r');
179            if ($stream === false) {
180                return;
181            }
182
183            $content = '';
184            $size    = 0;
185
186            while (($lineRaw = \fgets($stream, 1024)) !== false) {
187                // Limit json data to 1MB
188                if ($size > 1000000) {
189                    \fclose($stream);
190
191                    return;
192                }
193
194                $content .= $lineRaw;
195                $size    += \strlen($lineRaw);
196            }
197
198            \fclose($stream);
199
200            if (empty($content)) {
201                return;
202            }
203
204            \parse_str($content, $temp);
205            $this->data += $temp;
206            // @codeCoverageIgnoreEnd
207        } elseif (\stripos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false) {
208            // @codeCoverageIgnoreStart
209            // Tested but coverage doesn't show up
210            $stream   = \fopen('php://input', 'r');
211            $partInfo = null;
212            $boundary = null;
213
214            if ($stream === false) {
215                return;
216            }
217
218            // @codeCoverageIgnoreEnd
219            while (($lineRaw = \fgets($stream)) !== false) {
220                // @codeCoverageIgnoreStart
221                // Tested but coverage doesn't show up
222                if (\str_starts_with($lineRaw, '--')) {
223                    if ($boundary === null) {
224                        $boundary = \rtrim($lineRaw);
225                    }
226
227                    continue;
228                }
229
230                $line = \rtrim($lineRaw);
231
232                if ($line === '') {
233                    if (!empty($partInfo['Content-Disposition']['filename'])) { /* Is file */
234                        $tempdir                    = \sys_get_temp_dir();
235                        $name                       = $partInfo['Content-Disposition']['name'];
236                        $this->files[$name]         = [];
237                        $this->files[$name]['name'] = $partInfo['Content-Disposition']['filename'];
238                        $this->files[$name]['type'] = $partInfo['Content-Type']['value'] ?? null;
239
240                        $tempname = \tempnam($tempdir, 'oms_upl_');
241
242                        if ($tempname === false) {
243                            $this->files[$name]['error'] = \UPLOAD_ERR_NO_TMP_DIR;
244                            return;
245                        }
246
247                        $outFP = \fopen($tempname, 'wb');
248
249                        if ($outFP === false) {
250                            $this->files[$name]['error'] = \UPLOAD_ERR_CANT_WRITE;
251                            return;
252                        }
253
254                        $lastLine = null;
255                        while (($lineRaw = \fgets($stream, 4096)) !== false) {
256                            if ($lastLine !== null) {
257                                if ($boundary === null || \str_starts_with($lineRaw, $boundary)) {
258                                    break;
259                                }
260
261                                if (\fwrite($outFP, $lastLine) === false) {
262                                    $this->files[$name] = \UPLOAD_ERR_CANT_WRITE;
263                                    return;
264                                }
265                            }
266
267                            $lastLine = $lineRaw;
268                        }
269
270                        if ($lastLine !== null && \fwrite($outFP, \rtrim($lastLine, "\r\n")) === false) {
271                            $this->files[$name]['error'] = \UPLOAD_ERR_CANT_WRITE;
272                            return;
273                        }
274
275                        \fclose($outFP);
276
277                        $this->files[$name]['error']    = \UPLOAD_ERR_OK;
278                        $this->files[$name]['size']     = \filesize($tempname);
279                        $this->files[$name]['tmp_name'] = $tempname;
280                        // @codeCoverageIgnoreEnd
281                    } elseif ($partInfo !== null) { /* Is variable */
282                        // @codeCoverageIgnoreStart
283                        // Tested but coverage doesn't show up
284                        $fullValue = '';
285                        $lastLine  = null;
286
287                        while (($lineRaw = \fgets($stream)) !== false && $boundary !== null && !\str_starts_with($lineRaw, $boundary)) {
288                            if ($lastLine !== null) {
289                                $fullValue .= $lastLine;
290                            }
291
292                            $lastLine = $lineRaw;
293                        }
294
295                        if ($lastLine !== null) {
296                            $fullValue .= \rtrim($lastLine, "\r\n");
297                        }
298
299                        $this->data[$partInfo['Content-Disposition']['name']] = $fullValue;
300                        // @codeCoverageIgnoreEnd
301                    }
302
303                    $partInfo = null;
304
305                    continue;
306                }
307
308                // @codeCoverageIgnoreStart
309                // Tested but coverage doesn't show up
310                $delim = \strpos($line, ':');
311
312                if ($delim === false) {
313                    continue;
314                }
315
316                $headerKey = \substr($line, 0, $delim);
317                $headerVal = \substr($line, $delim + 1);
318
319                $header  = [];
320                $regex   = '/(^|;)\s*(?P<name>[^=:,;\s"]*):?(=("(?P<quotedValue>[^"]*(\\.[^"]*)*)")|(\s*(?P<value>[^=,;\s"]*)))?/mx';
321                $matches = null;
322
323                \preg_match_all($regex, $headerVal, $matches, \PREG_SET_ORDER);
324
325                $length = \count($matches);
326                for ($i = 0; $i < $length; ++$i) {
327                    $match       = $matches[$i];
328                    $name        = $match['name'];
329                    $quotedValue = $match['quotedValue'];
330
331                    $value = empty($quotedValue) ? $value = $match['value'] : \stripcslashes($quotedValue);
332
333                    if ($name === $headerKey && $i === 0) {
334                        $name = 'value';
335                    } elseif ($value === '') {
336                        $value = $name;
337                        $name  = 'value';
338                    }
339
340                    $header[$name] = $value;
341                }
342
343                $partInfo[$headerKey] = $header;
344                // @codeCoverageIgnoreEnd
345            }
346
347            \fclose($stream);
348        }
349    }
350
351    /**
352     * Set request method.
353     *
354     * @param string $method Request method
355     *
356     * @return void
357     *
358     * @since 1.0.0
359     */
360    public function setMethod(string $method) : void
361    {
362        $this->method = $method;
363    }
364
365    /**
366     * Get request language
367     *
368     * @return string
369     *
370     * @since 1.0.0
371     */
372    private function getRequestLanguage() : string
373    {
374        $locale               = $this->getLocale();
375        $firstLocalComponents = \explode('_', $locale);
376        $language             = \strtolower($firstLocalComponents[0]);
377
378        return ISO639x1Enum::isValidValue($language) ? $language : 'en';
379    }
380
381    /**
382     * Get request country
383     *
384     * @return string
385     *
386     * @since 1.0.0
387     */
388    private function getRequestCountry() : string
389    {
390        $locale               = $this->getLocale();
391        $firstLocalComponents = \explode('_', $locale);
392        $country              = \strtoupper($firstLocalComponents[1] ?? '');
393
394        return ISO3166TwoEnum::isValidValue($country) ? $country : 'US';
395    }
396
397    /**
398     * Get request locale
399     *
400     * @return string
401     *
402     * @since 1.0.0
403     */
404    public function getLocale() : string
405    {
406        if (!empty($this->locale)) {
407            return $this->locale = $this->header->l11n->language . '_' . $this->header->l11n->country;
408        }
409
410        if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
411            return 'en_US';
412        }
413
414        // @codeCoverageIgnoreStart
415        $components   = \explode(';', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
416        $locals       = \stripos($components[0], ',') !== false
417            ? $locals = \explode(',', $components[0])
418            : $components;
419        // @codeCoverageIgnoreEnd
420
421        $this->locale = \strtr($locals[0], '-', '_'); // @codeCoverageIgnore
422
423        return $this->locale;
424    }
425
426    /**
427     * Clean up globals that musn't be used any longer
428     *
429     * @return void
430     *
431     * @since 1.0.0
432     */
433    public static function cleanupGlobals() : void
434    {
435        unset($_FILES);
436        unset($_GET);
437        unset($_POST);
438        unset($_REQUEST);
439    }
440
441    /**
442     * Create request from superglobals.
443     *
444     * @return HttpRequest
445     *
446     * @since 1.0.0
447     */
448    public static function createFromSuperglobals() : self
449    {
450        $request =  new self();
451        $request->initRequest();
452
453        return $request;
454    }
455
456    /**
457     * Set request uri.
458     *
459     * @param UriInterface $uri Uri
460     *
461     * @return void
462     *
463     * @since 1.0.0
464     */
465    public function setUri(UriInterface $uri) : void
466    {
467        $this->uri   = $uri;
468        $this->data += $uri->getQueryArray();
469    }
470
471    /**
472     * Create request hashs of current request
473     *
474     * The hashes are based on the request path and can be used as unique id.
475     *
476     * @param int $start Start hash from n-th path element
477     *
478     * @return void
479     *
480     * @since 1.0.0
481     */
482    public function createRequestHashs(int $start = 0) : void
483    {
484        $this->hash = [\sha1('')];
485        $pathArray  = $this->uri->getPathElements();
486        $pathLength = \count($pathArray);
487
488        for ($i = $start; $i < $pathLength; ++$i) {
489            if ($pathArray[$i] === '') {
490                continue;
491            }
492
493            $paths = [];
494            for ($j = $start; $j <= $i; ++$j) {
495                $paths[] = $pathArray[$j];
496            }
497
498            $this->hash[] = \sha1(\implode('', $paths));
499        }
500    }
501
502    /**
503     * Is Mobile
504     *
505     * @return bool
506     *
507     * @since 1.0.0
508     */
509    public function isMobile() : bool
510    {
511        $useragent = $_SERVER['HTTP_USER_AGENT'] ?? '';
512
513        if (\preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i', $useragent) || \preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i', $useragent)) {
514            return true; // @codeCoverageIgnore
515        }
516
517        return false;
518    }
519
520    /**
521     * {@inheritdoc}
522     */
523    public function getRequestInfo() : array
524    {
525        if (!isset($this->info)) {
526            $this->info['browser'] = $this->getBrowser();
527            $this->info['os']      = $this->getOS();
528        }
529
530        return $this->info;
531    }
532
533    /**
534     * Determine request browser.
535     *
536     * @return string
537     *
538     * @since 1.0.0
539     */
540    public function getBrowser() : string
541    {
542        if (!isset($this->browser)) {
543            $arr           = BrowserType::getConstants();
544            $httpUserAgent = \strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
545
546            foreach ($arr as $key => $val) {
547                if (\stripos($httpUserAgent, $val)) {
548                    // @codeCoverageIgnoreStart
549                    $this->browser = $val;
550
551                    return $this->browser;
552                    // @codeCoverageIgnoreEnd
553                }
554            }
555
556            $this->browser = BrowserType::UNKNOWN;
557        }
558
559        return $this->browser;
560    }
561
562    /**
563     * Set browser type
564     *
565     * @param string $browser Browser type
566     *
567     * @return void
568     *
569     * @since 1.0.0
570     */
571    public function setBrowser(string $browser) : void
572    {
573        $this->browser = $browser;
574    }
575
576    /**
577     * Determine request OS.
578     *
579     * @return string
580     *
581     * @since 1.0.0
582     */
583    public function getOS() : string
584    {
585        if (!isset($this->os)) {
586            $arr           = OSType::getConstants();
587            $httpUserAgent = \strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
588
589            foreach ($arr as $val) {
590                if (\stripos($httpUserAgent, $val)) {
591                    // @codeCoverageIgnoreStart
592                    $this->os = $val;
593
594                    return $this->os;
595                    // @codeCoverageIgnoreEnd
596                }
597            }
598
599            $this->os = OSType::UNKNOWN;
600        }
601
602        return $this->os;
603    }
604
605    /**
606     * Set OS type
607     *
608     * @param string $os OS type
609     *
610     * @return void
611     *
612     * @since 1.0.0
613     */
614    public function setOS(string $os) : void
615    {
616        $this->os = $os;
617    }
618
619    /**
620     * {@inheritdoc}
621     */
622    public function getOrigin() : string
623    {
624        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
625    }
626
627    /**
628     * Is request made via https.
629     *
630     * @param int $port Secure port
631     *
632     * @return bool
633     *
634     * @throws \OutOfRangeException This exception is thrown if the port is out of range
635     *
636     * @since 1.0.0
637     */
638    public static function isHttps(int $port = 443) : bool
639    {
640        if ($port < 1 || $port > 65535) {
641            throw new \OutOfRangeException('Value "' . $port . '" is out of range.');
642        }
643
644        return (!empty($_SERVER['HTTPS'] ?? '') && ($_SERVER['HTTPS'] ?? '') !== 'off')
645            || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
646            || (($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on')
647            || ($_SERVER['SERVER_PORT'] ?? '') === $port;
648    }
649
650    /**
651     * {@inheritdoc}
652     */
653    public function getBody() : string
654    {
655        $body = \file_get_contents('php://input');
656        return $body === false ? '' : $body;
657    }
658
659    /**
660     * Get route verb.
661     *
662     * @return int
663     *
664     * @throws \Exception
665     *
666     * @since 1.0.0
667     */
668    public function getRouteVerb() : int
669    {
670        switch ($this->getMethod()) {
671            case RequestMethod::GET:
672                return RouteVerb::GET;
673            case RequestMethod::PUT:
674                return RouteVerb::PUT;
675            case RequestMethod::POST:
676                return RouteVerb::SET;
677            case RequestMethod::DELETE:
678                return RouteVerb::DELETE;
679            default:
680                throw new \Exception();
681        }
682    }
683
684    /**
685     * Get referer.
686     *
687     * @return string
688     *
689     * @since 1.0.0
690     */
691    public function getReferer() : string
692    {
693        return $_SERVER['HTTP_REFERER'] ?? '';
694    }
695
696    /**
697     * Get request method.
698     *
699     * @return string
700     *
701     * @since 1.0.0
702     */
703    public function getMethod() : string
704    {
705        if (!isset($this->method)) {
706            $this->method = $_SERVER['REQUEST_METHOD'] ?? RequestMethod::GET;
707        }
708
709        return $this->method;
710    }
711
712    /**
713     * Perform rest request
714     *
715     * @return HttpResponse
716     *
717     * @since 1.0.0
718     */
719    public function rest() : HttpResponse
720    {
721        return Rest::request($this);
722    }
723
724    /**
725     * {@inheritdoc}
726     */
727    public function __toString() : string
728    {
729        if ($this->getMethod() === RequestMethod::GET && !empty($this->data)) {
730            return $this->uri->__toString()
731                . (\parse_url($this->uri->__toString(), \PHP_URL_QUERY) ? '&' : '?')
732                . \http_build_query($this->data);
733        }
734
735        return parent::__toString();
736    }
737}