• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / http / 20938023192

12 Jan 2026 10:59PM UTC coverage: 98.774% (-0.06%) from 98.833%
20938023192

push

github

natanfelles
Avoid unnecessary calls to the Request::getInputFiles method

1 of 1 new or added line in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

1611 of 1631 relevant lines covered (98.77%)

14.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.77
/src/Request.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework HTTP Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\HTTP;
11

12
use BadMethodCallException;
13
use Framework\Helpers\ArraySimple;
14
use InvalidArgumentException;
15
use JetBrains\PhpStorm\ArrayShape;
16
use JetBrains\PhpStorm\Pure;
17
use LogicException;
18
use Override;
19
use RequestParseBodyException;
20
use stdClass;
21
use UnexpectedValueException;
22

23
/**
24
 * Class Request.
25
 *
26
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#HTTP_Requests
27
 *
28
 * @package http
29
 */
30
class Request extends Message implements RequestInterface
31
{
32
    /**
33
     * @var array<string,UploadedFile|array<mixed>>|null
34
     */
35
    protected ?array $files = null;
36
    /**
37
     * @var array<int,array<mixed>>|null
38
     */
39
    protected ?array $parsedBody = null;
40
    /**
41
     * @var array<string,mixed>|null
42
     */
43
    protected ?array $parseBodyOptions = null;
44
    /**
45
     * HTTP Authorization Header parsed.
46
     *
47
     * @var array<string,string|null>|null
48
     */
49
    protected ?array $auth = null;
50
    /**
51
     * @var string|null Basic or Digest
52
     */
53
    protected ?string $authType = null;
54
    protected string $host;
55
    protected int $port;
56
    /**
57
     * Request X-Request-ID header.
58
     */
59
    protected false | string $id;
60
    /**
61
     * @var array<string,array<mixed>|null>
62
     */
63
    protected array $negotiation = [
64
        'ACCEPT' => null,
65
        'CHARSET' => null,
66
        'ENCODING' => null,
67
        'LANGUAGE' => null,
68
    ];
69
    protected URL | false $referrer;
70
    protected UserAgent | false $userAgent;
71
    protected bool $isAjax;
72
    /**
73
     * Tell if is a HTTPS connection.
74
     *
75
     * @var bool
76
     */
77
    protected bool $isSecure;
78
    protected int $jsonFlags = 0;
79
    protected string $ipKey = 'REMOTE_ADDR';
80

81
    /**
82
     * Request constructor.
83
     *
84
     * @param array<string> $allowedHosts set allowed hosts if your
85
     * server don't serve by Host header, as Nginx do
86
     *
87
     * @throws UnexpectedValueException if invalid Host
88
     */
89
    public function __construct(array $allowedHosts = [])
90
    {
91
        if ($allowedHosts) {
162✔
92
            $this->validateHost($allowedHosts);
129✔
93
        }
94
        $this->prepareStatusLine();
162✔
95
    }
96

97
    /**
98
     * @param string $method
99
     * @param array<int,mixed> $arguments
100
     *
101
     * @throws BadMethodCallException for method not allowed or method not found
102
     *
103
     * @return static
104
     */
105
    public function __call(string $method, array $arguments)
106
    {
107
        if ($method === 'setBody') {
6✔
108
            return $this->setBody(...$arguments);
4✔
109
        }
110
        if (\method_exists($this, $method)) {
2✔
111
            throw new BadMethodCallException("Method not allowed: {$method}");
1✔
112
        }
113
        throw new BadMethodCallException("Method not found: {$method}");
1✔
114
    }
115

116
    #[Override]
117
    public function __toString() : string
118
    {
119
        if ($this->parseContentType() === 'multipart/form-data') {
2✔
120
            $this->setBody($this->getMultipartBody());
1✔
121
        }
122
        return parent::__toString();
2✔
123
    }
124

125
    /**
126
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data
127
     *
128
     * @return string
129
     */
130
    protected function getMultipartBody() : string
131
    {
132
        $bodyParts = [];
1✔
133
        /**
134
         * @var array<string,string> $post
135
         */
136
        $post = ArraySimple::convert($this->getPost());
1✔
137
        foreach ($post as $field => $value) {
1✔
138
            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
1✔
139
            $bodyParts[] = \implode("\r\n", [
1✔
140
                "Content-Disposition: form-data; name=\"{$field}\"",
1✔
141
                '',
1✔
142
                $value,
1✔
143
            ]);
1✔
144
        }
145
        /**
146
         * @var array<string,UploadedFile> $files
147
         */
148
        $files = ArraySimple::convert($this->getFiles());
1✔
149
        foreach ($files as $field => $file) {
1✔
150
            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
1✔
151
            $filename = \htmlspecialchars($file->getName(), \ENT_QUOTES | \ENT_HTML5);
1✔
152
            $getContentsOf = $file->isMoved() ? $file->getDestination() : $file->getTmpName();
1✔
153
            $data = '';
1✔
154
            if ($getContentsOf !== '') {
1✔
155
                $data = \file_get_contents($getContentsOf);
1✔
156
            }
157
            $bodyParts[] = \implode("\r\n", [
1✔
158
                "Content-Disposition: form-data; name=\"{$field}\"; filename=\"{$filename}\"",
1✔
159
                'Content-Type: ' . $file->getClientType(),
1✔
160
                '',
1✔
161
                $data,
1✔
162
            ]);
1✔
163
        }
164
        $boundary = \explode(';', $this->getContentType(), 2);
1✔
165
        $boundary = \trim($boundary[1]);
1✔
166
        $boundary = \substr($boundary, \strlen('boundary='));
1✔
167
        foreach ($bodyParts as &$part) {
1✔
168
            $part = "--{$boundary}\r\n{$part}";
1✔
169
        }
170
        unset($part);
1✔
171
        $bodyParts[] = "--{$boundary}--";
1✔
172
        $bodyParts[] = '';
1✔
173
        $bodyParts = \implode("\r\n", $bodyParts);
1✔
174
        /**
175
         * Uncomment the code below to make a raw test.
176
         *
177
         * @see \Tests\HTTP\RequestTest::testToStringMultipart()
178
         */
179
        /*
180
        $serverLength = (string) $_SERVER['CONTENT_LENGTH'];
181
        $algoLength = (string) \strlen($bodyParts);
182
        if ($serverLength !== $algoLength) {
183
            throw new \Exception(
184
                '$_SERVER CONTENT_LENGTH is ' . $serverLength
185
                . ', but the algorithm calculated ' . $algoLength
186
            );
187
        }
188
        */
189
        return $bodyParts;
1✔
190
    }
191

192
    /**
193
     * Check if Host header is allowed.
194
     *
195
     * @see https://expressionengine.com/blog/http-host-and-server-name-security-issues
196
     * @see http://nginx.org/en/docs/http/request_processing.html
197
     *
198
     * @param array<string> $allowedHosts
199
     */
200
    protected function validateHost(array $allowedHosts) : void
201
    {
202
        $host = $_SERVER['HTTP_HOST'] ?? null;
129✔
203
        if (!\in_array($host, $allowedHosts, true)) {
129✔
204
            throw new UnexpectedValueException('Invalid Host: ' . $host);
1✔
205
        }
206
    }
207

208
    protected function prepareStatusLine() : void
209
    {
210
        $this->setProtocol($_SERVER['SERVER_PROTOCOL']);
162✔
211
        $this->setMethod($_SERVER['REQUEST_METHOD']);
162✔
212
        $url = $this->isSecure() ? 'https' : 'http';
162✔
213
        $url .= '://' . $_SERVER['HTTP_HOST'];
162✔
214
        $url .= $_SERVER['REQUEST_URI'];
162✔
215
        $this->setUrl($url);
162✔
216
        $this->setHost($this->getUrl()->getHost());
162✔
217
    }
218

219
    public function getHeader(string $name) : ?string
220
    {
221
        $this->prepareHeaders();
39✔
222
        return $this->headers[\strtolower($name)] ?? null;
39✔
223
    }
224

225
    /**
226
     * @return array<string,string>
227
     */
228
    public function getHeaders() : array
229
    {
230
        $this->prepareHeaders();
15✔
231
        return $this->headers;
15✔
232
    }
233

234
    protected function prepareHeaders() : void
235
    {
236
        if (!empty($this->headers)) {
41✔
237
            return;
34✔
238
        }
239
        foreach ($_SERVER as $name => $value) {
28✔
240
            if (\str_starts_with($name, 'HTTP_')) {
28✔
241
                $name = \strtr(\substr($name, 5), ['_' => '-']);
28✔
242
                $this->setHeader($name, $value);
28✔
243
            }
244
        }
245
    }
246

247
    public function getCookie(string $name) : ?Cookie
248
    {
249
        $this->prepareCookies();
1✔
250
        return $this->cookies[$name] ?? null;
1✔
251
    }
252

253
    /**
254
     * Get all Cookies.
255
     *
256
     * @return array<string,Cookie>
257
     */
258
    public function getCookies() : array
259
    {
260
        $this->prepareCookies();
1✔
261
        return $this->cookies;
1✔
262
    }
263

264
    protected function prepareCookies() : void
265
    {
266
        if (!empty($this->cookies)) {
2✔
267
            return;
1✔
268
        }
269
        foreach ($_COOKIE as $name => $value) {
2✔
270
            $this->setCookie(new Cookie($name, $value));
2✔
271
        }
272
    }
273

274
    /**
275
     * @see https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input
276
     *
277
     * @return string
278
     */
279
    public function getBody() : string
280
    {
281
        if (!isset($this->body)) {
18✔
282
            $this->body = (string) \file_get_contents('php://input');
14✔
283
        }
284
        return $this->body;
18✔
285
    }
286

287
    protected function prepareFiles() : void
288
    {
289
        if (isset($this->files)) {
16✔
290
            return;
13✔
291
        }
292
        $this->files = $this->getInputFiles();
16✔
293
    }
294

295
    /**
296
     * @param int $type
297
     * @param string|null $name
298
     * @param int|null $filter
299
     * @param array<int,int>|int $options
300
     *
301
     * @see https://www.php.net/manual/en/function.filter-var
302
     * @see https://www.php.net/manual/en/filter.filters.php
303
     *
304
     * @return mixed
305
     */
306
    protected function filterInput(
307
        int $type,
308
        ?string $name = null,
309
        ?int $filter = null,
310
        array | int $options = 0
311
    ) : mixed {
312
        $input = match ($type) {
25✔
313
            \INPUT_POST => $_POST,
10✔
314
            \INPUT_GET => $_GET,
1✔
315
            \INPUT_COOKIE => $_COOKIE,
1✔
316
            \INPUT_ENV => $_ENV,
1✔
317
            \INPUT_SERVER => $_SERVER,
13✔
318
            default => throw new InvalidArgumentException('Invalid input type: ' . $type)
1✔
319
        };
25✔
320
        if ($name !== null) {
24✔
321
            $input = \in_array($type, [\INPUT_POST, \INPUT_GET], true)
21✔
322
                ? ArraySimple::value($name, $input)
7✔
323
                : $input[$name] ?? null;
14✔
324
        }
325
        if ($filter !== null) {
24✔
326
            $input = \filter_var($input, $filter, $options);
2✔
327
        }
328
        return $input;
24✔
329
    }
330

331
    /**
332
     * Force an HTTPS connection on same URL.
333
     */
334
    public function forceHttps() : void
335
    {
336
        if (!$this->isSecure()) {
2✔
337
            \header(
1✔
338
                'Location: ' . $this->getUrl()->setScheme('https'),
1✔
339
                true,
1✔
340
                Status::MOVED_PERMANENTLY
1✔
341
            );
1✔
342
            if (!\defined('TESTING')) {
1✔
343
                // @codeCoverageIgnoreStart
344
                exit;
345
                // @codeCoverageIgnoreEnd
346
            }
347
        }
348
    }
349

350
    /**
351
     * Get the Authorization type.
352
     *
353
     * @return string|null Basic, Bearer, Digest or null for none
354
     */
355
    public function getAuthType() : ?string
356
    {
357
        if ($this->authType === null) {
4✔
358
            $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
4✔
359
            if ($auth) {
4✔
360
                $this->parseAuth($auth);
3✔
361
            }
362
        }
363
        return $this->authType;
4✔
364
    }
365

366
    /**
367
     * Get Basic authorization.
368
     *
369
     * @return array<string>|null Two keys: username and password
370
     */
371
    #[ArrayShape(['username' => 'string|null', 'password' => 'string|null'])]
372
    public function getBasicAuth() : ?array
373
    {
374
        return $this->getAuthType() === 'Basic'
2✔
375
            ? $this->auth
1✔
376
            : null;
2✔
377
    }
378

379
    /**
380
     * Get Bearer authorization.
381
     *
382
     * @return array<string>|null One key: token
383
     */
384
    #[ArrayShape(['token' => 'string|null'])]
385
    public function getBearerAuth() : ?array
386
    {
387
        return $this->getAuthType() === 'Bearer'
2✔
388
            ? $this->auth
1✔
389
            : null;
2✔
390
    }
391

392
    /**
393
     * Get Digest authorization.
394
     *
395
     * @return array<string>|null Nine keys: username, realm, nonce, uri,
396
     * response, opaque, qop, nc, cnonce
397
     */
398
    #[ArrayShape([
399
        'username' => 'string|null',
400
        'realm' => 'string|null',
401
        'nonce' => 'string|null',
402
        'uri' => 'string|null',
403
        'response' => 'string|null',
404
        'opaque' => 'string|null',
405
        'qop' => 'string|null',
406
        'nc' => 'string|null',
407
        'cnonce' => 'string|null',
408
    ])]
409
    public function getDigestAuth() : ?array
410
    {
411
        return $this->getAuthType() === 'Digest'
2✔
412
            ? $this->auth
1✔
413
            : null;
2✔
414
    }
415

416
    /**
417
     * @param string $authorization
418
     *
419
     * @return array<string,string|null>
420
     */
421
    protected function parseAuth(string $authorization) : array
422
    {
423
        $this->auth = [];
3✔
424
        [$type, $attributes] = \array_pad(\explode(' ', $authorization, 2), 2, null);
3✔
425
        if ($type === 'Basic') {
3✔
426
            $this->authType = $type;
1✔
427
            $this->auth = $this->parseBasicAuth($attributes);
1✔
428
        } elseif ($type === 'Bearer') {
2✔
429
            $this->authType = $type;
1✔
430
            $this->auth = $this->parseBearerAuth($attributes);
1✔
431
        } elseif ($type === 'Digest') {
1✔
432
            $this->authType = $type;
1✔
433
            $this->auth = $this->parseDigestAuth($attributes);
1✔
434
        }
435
        return $this->auth;
3✔
436
    }
437

438
    /**
439
     * @param string $attributes
440
     *
441
     * @return array<string,string|null>
442
     */
443
    #[ArrayShape(['username' => 'string|null', 'password' => 'string|null'])]
444
    #[Pure]
445
    protected function parseBasicAuth(string $attributes) : array
446
    {
447
        $data = [
1✔
448
            'username' => null,
1✔
449
            'password' => null,
1✔
450
        ];
1✔
451
        $attributes = \base64_decode($attributes);
1✔
452
        if ($attributes) {
1✔
453
            [
1✔
454
                $data['username'],
1✔
455
                $data['password'],
1✔
456
            ] = \array_pad(\explode(':', $attributes, 2), 2, null);
1✔
457
        }
458
        return $data;
1✔
459
    }
460

461
    /**
462
     * @param string $attributes
463
     *
464
     * @return array<string,string|null>
465
     */
466
    #[ArrayShape(['token' => 'string|null'])]
467
    #[Pure]
468
    protected function parseBearerAuth(string $attributes) : array
469
    {
470
        $data = [
1✔
471
            'token' => null,
1✔
472
        ];
1✔
473
        if ($attributes) {
1✔
474
            $data['token'] = $attributes;
1✔
475
        }
476
        return $data;
1✔
477
    }
478

479
    /**
480
     * @param string $attributes
481
     *
482
     * @return array<string,string|null>
483
     */
484
    #[ArrayShape([
485
        'username' => 'string|null',
486
        'realm' => 'string|null',
487
        'nonce' => 'string|null',
488
        'uri' => 'string|null',
489
        'response' => 'string|null',
490
        'opaque' => 'string|null',
491
        'qop' => 'string|null',
492
        'nc' => 'string|null',
493
        'cnonce' => 'string|null',
494
    ])]
495
    protected function parseDigestAuth(string $attributes) : array
496
    {
497
        $data = [
1✔
498
            'username' => null,
1✔
499
            'realm' => null,
1✔
500
            'nonce' => null,
1✔
501
            'uri' => null,
1✔
502
            'response' => null,
1✔
503
            'opaque' => null,
1✔
504
            'qop' => null,
1✔
505
            'nc' => null,
1✔
506
            'cnonce' => null,
1✔
507
        ];
1✔
508
        \preg_match_all(
1✔
509
            '#(username|realm|nonce|uri|response|opaque|qop|nc|cnonce)=(?:([\'"])([^\2]+?)\2|([^\s,]+))#',
1✔
510
            $attributes,
1✔
511
            $matches,
1✔
512
            \PREG_SET_ORDER
1✔
513
        );
1✔
514
        foreach ($matches as $match) {
1✔
515
            if (isset($match[3])) {
1✔
516
                $data[$match[1]] = $match[3] ?: $match[4] ?? '';
1✔
517
            }
518
        }
519
        return $data;
1✔
520
    }
521

522
    /**
523
     * Get the Parsed Body or part of it.
524
     *
525
     * @param string|null $name
526
     * @param int|null $filter
527
     * @param array<int,int>|int $filterOptions
528
     *
529
     * @throws RequestParseBodyException
530
     *
531
     * @return array<mixed>|mixed|string|null
532
     */
533
    public function getParsedBody(
534
        ?string $name = null,
535
        ?int $filter = null,
536
        array | int $filterOptions = 0
537
    ) : mixed {
538
        if ($this->isMethod(Method::POST)) {
8✔
539
            return $this->getPost($name, $filter, $filterOptions);
8✔
540
        }
541
        return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
542
    }
543

544
    /**
545
     * @param string|null $name
546
     * @param int|null $filter
547
     * @param array<int,int>|int $filterOptions
548
     *
549
     * @throws RequestParseBodyException
550
     *
551
     * @return array<mixed>|mixed|string|null
552
     */
553
    protected function getFilteredParsedBody(
554
        ?string $name = null,
555
        ?int $filter = null,
556
        array | int $filterOptions = 0
557
    ) : mixed {
558
        if (!$this->isParsedBody()) {
3✔
559
            $this->parseBody($this->getParseBodyOptions());
3✔
560
        }
561
        $variable = $name === null
3✔
562
            ? $this->parsedBody[0]
3✔
563
            : ArraySimple::value($name, $this->parsedBody[0]);
3✔
564
        return $filter !== null
3✔
565
            ? \filter_var($variable, $filter, $filterOptions)
1✔
566
            : $variable;
3✔
567
    }
568

569
    /**
570
     * @param array<string,mixed>|null $options
571
     *
572
     * @throws RequestParseBodyException
573
     * @throws LogicException
574
     *
575
     * @return static
576
     */
577
    public function parseBody(?array $options = null) : static
578
    {
579
        if ($this->isParsedBody()) {
5✔
580
            throw new LogicException('Parse error: the request body has already been parsed');
1✔
581
        }
582
        if (!$this->isForm()) {
5✔
583
            $this->parsedBody = [[], []];
3✔
584
            return $this;
3✔
585
        }
586
        $options ??= $this->getParseBodyOptions();
3✔
587
        $this->parsedBody = $this->requestParseBody($options);
3✔
588
        return $this;
3✔
589
    }
590

591
    /**
592
     * @param array<string,mixed>|null $options
593
     *
594
     * @throws RequestParseBodyException
595
     *
596
     * @return array<int,array<mixed>>
597
     */
598
    protected function requestParseBody(?array $options = null) : array
599
    {
600
        return \request_parse_body($options);
1✔
601
    }
602

603
    public function isParsedBody() : bool
604
    {
605
        return isset($this->parsedBody);
21✔
606
    }
607

608
    /**
609
     * @return array<string,mixed>|null
610
     */
611
    public function getParseBodyOptions() : ?array
612
    {
613
        return $this->parseBodyOptions;
4✔
614
    }
615

616
    /**
617
     * @param array<string,mixed>|null $options
618
     *
619
     * @see https://www.php.net/manual/en/function.request-parse-body.php#refsect1-function.request-parse-body-parameters
620
     */
621
    public function setParseBodyOptions(?array $options) : static
622
    {
623
        $this->parseBodyOptions = $options;
1✔
624
        return $this;
1✔
625
    }
626

627
    /**
628
     * Get the request body as JSON.
629
     *
630
     * @param bool|null $associative When true, JSON objects will be returned as
631
     * associative arrays; when false, JSON objects will be returned as objects.
632
     * When null, JSON objects will be returned as associative arrays or objects
633
     * depending on whether JSON_OBJECT_AS_ARRAY is set in the flags.
634
     * @param int|null $flags [optional] <p>
635
     *  Bitmask consisting of <b>JSON_BIGINT_AS_STRING</b>,
636
     *  <b>JSON_INVALID_UTF8_IGNORE</b>,
637
     *  <b>JSON_INVALID_UTF8_SUBSTITUTE</b>,
638
     *  <b>JSON_OBJECT_AS_ARRAY</b>,
639
     *  <b>JSON_THROW_ON_ERROR</b>.
640
     *  </p>
641
     *  <p>Default is none when null.</p>
642
     * @param int<1,max> $depth user specified recursion depth
643
     *
644
     * @see https://www.php.net/manual/en/function.json-decode.php
645
     * @see https://www.php.net/manual/en/json.constants.php
646
     *
647
     * @return array<string,mixed>|false|stdClass If option JSON_THROW_ON_ERROR
648
     * is not set, return false if json_decode fail. Otherwise, return a
649
     * stdClass instance, or an array if the $associative argument is passed as
650
     * true.
651
     */
652
    public function getJson(
653
        ?bool $associative = null,
654
        ?int $flags = null,
655
        int $depth = 512
656
    ) : array | false | stdClass {
657
        if ($flags === null) {
2✔
658
            $flags = $this->getJsonFlags();
2✔
659
        }
660
        $body = \json_decode($this->getBody(), $associative, $depth, $flags);
2✔
661
        if (\json_last_error() !== \JSON_ERROR_NONE) {
2✔
662
            return false;
1✔
663
        }
664
        return $body;
1✔
665
    }
666

667
    /**
668
     * @param string $type
669
     *
670
     * @return array<int,string>
671
     */
672
    protected function getNegotiableValues(string $type) : array
673
    {
674
        if ($this->negotiation[$type]) {
5✔
675
            return $this->negotiation[$type];
4✔
676
        }
677
        $header = $_SERVER['HTTP_ACCEPT' . ($type !== 'ACCEPT' ? '_' . $type : '')] ?? null;
5✔
678
        $this->negotiation[$type] = \array_keys(static::parseQualityValues(
5✔
679
            $header
5✔
680
        ));
5✔
681
        $this->negotiation[$type] = \array_map('\strtolower', $this->negotiation[$type]);
5✔
682
        return $this->negotiation[$type];
5✔
683
    }
684

685
    /**
686
     * @param string $type
687
     * @param array<int,string> $negotiable
688
     *
689
     * @return string
690
     */
691
    protected function negotiate(string $type, array $negotiable) : string
692
    {
693
        $negotiable = \array_map('\strtolower', $negotiable);
4✔
694
        foreach ($this->getNegotiableValues($type) as $item) {
4✔
695
            if (\in_array($item, $negotiable, true)) {
4✔
696
                return $item;
4✔
697
            }
698
        }
699
        return $negotiable[0];
4✔
700
    }
701

702
    /**
703
     * Get the mime types of the Accept header.
704
     *
705
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
706
     *
707
     * @return array<int,string>
708
     */
709
    public function getAccepts() : array
710
    {
711
        return $this->getNegotiableValues('ACCEPT');
2✔
712
    }
713

714
    /**
715
     * Negotiate the Accept header.
716
     *
717
     * @param array<int,string> $negotiable Allowed mime types
718
     *
719
     * @return string The negotiated mime type
720
     */
721
    public function negotiateAccept(array $negotiable) : string
722
    {
723
        return $this->negotiate('ACCEPT', $negotiable);
1✔
724
    }
725

726
    /**
727
     * Get the Accept-Charset's.
728
     *
729
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
730
     *
731
     * @return array<int,string>
732
     */
733
    public function getCharsets() : array
734
    {
735
        return $this->getNegotiableValues('CHARSET');
1✔
736
    }
737

738
    /**
739
     * Negotiate the Accept-Charset.
740
     *
741
     * @param array<int,string> $negotiable Allowed charsets
742
     *
743
     * @return string The negotiated charset
744
     */
745
    public function negotiateCharset(array $negotiable) : string
746
    {
747
        return $this->negotiate('CHARSET', $negotiable);
1✔
748
    }
749

750
    /**
751
     * Get the Accept-Encoding.
752
     *
753
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
754
     *
755
     * @return array<int,string>
756
     */
757
    public function getEncodings() : array
758
    {
759
        return $this->getNegotiableValues('ENCODING');
1✔
760
    }
761

762
    /**
763
     * Negotiate the Accept-Encoding.
764
     *
765
     * @param array<int,string> $negotiable The allowed encodings
766
     *
767
     * @return string The negotiated encoding
768
     */
769
    public function negotiateEncoding(array $negotiable) : string
770
    {
771
        return $this->negotiate('ENCODING', $negotiable);
1✔
772
    }
773

774
    /**
775
     * Get the Accept-Language's.
776
     *
777
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
778
     *
779
     * @return array<int,string>
780
     */
781
    public function getLanguages() : array
782
    {
783
        return $this->getNegotiableValues('LANGUAGE');
1✔
784
    }
785

786
    /**
787
     * Negotiated the Accept-Language.
788
     *
789
     * @param array<int,string> $negotiable Allowed languages
790
     *
791
     * @return string The negotiated language
792
     */
793
    public function negotiateLanguage(array $negotiable) : string
794
    {
795
        return $this->negotiate('LANGUAGE', $negotiable);
1✔
796
    }
797

798
    /**
799
     * Get the Content-Type header value.
800
     *
801
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
802
     *
803
     * @return string|null
804
     */
805
    #[Pure]
806
    public function getContentType() : ?string
807
    {
808
        return $_SERVER['HTTP_CONTENT_TYPE'] ?? null;
2✔
809
    }
810

811
    /**
812
     * @param string|null $name
813
     * @param int|null $filter
814
     * @param array<int,int>|int $filterOptions
815
     *
816
     * @return mixed
817
     */
818
    public function getEnv(
819
        ?string $name = null,
820
        ?int $filter = null,
821
        array | int $filterOptions = 0
822
    ) : mixed {
823
        return $this->filterInput(\INPUT_ENV, $name, $filter, $filterOptions);
1✔
824
    }
825

826
    /**
827
     * @return array<string,UploadedFile|array<mixed>>
828
     */
829
    public function getFiles() : array
830
    {
831
        $this->prepareFiles();
4✔
832
        return $this->files;
4✔
833
    }
834

835
    public function hasFiles() : bool
836
    {
837
        $this->prepareFiles();
13✔
838
        return !empty($this->files);
13✔
839
    }
840

841
    public function getFile(string $name) : ?UploadedFile
842
    {
843
        $this->prepareFiles();
1✔
844
        $file = ArraySimple::value($name, $this->files);
1✔
845
        return \is_array($file) ? null : $file;
1✔
846
    }
847

848
    /**
849
     * Get the URL GET queries.
850
     *
851
     * @param string|null $name
852
     * @param int|null $filter
853
     * @param array<int,int>|int $filterOptions
854
     *
855
     * @return mixed
856
     */
857
    public function getGet(
858
        ?string $name = null,
859
        ?int $filter = null,
860
        array | int $filterOptions = 0
861
    ) : mixed {
862
        return $this->filterInput(\INPUT_GET, $name, $filter, $filterOptions);
1✔
863
    }
864

865
    /**
866
     * @return string
867
     */
868
    #[Pure]
869
    public function getHost() : string
870
    {
871
        return $this->host;
2✔
872
    }
873

874
    /**
875
     * Get the X-Request-ID header.
876
     *
877
     * @return string|null
878
     */
879
    public function getId() : ?string
880
    {
881
        if (isset($this->id)) {
2✔
882
            return $this->id === false ? null : $this->id;
2✔
883
        }
884
        $this->id = $_SERVER['HTTP_X_REQUEST_ID'] ?? false;
2✔
885
        return $this->getId();
2✔
886
    }
887

888
    /**
889
     * Get the connection IP.
890
     *
891
     * @return string
892
     */
893
    public function getIp() : string
894
    {
895
        return $_SERVER[$this->getIpKey()];
14✔
896
    }
897

898
    /**
899
     * Set the key used to get the IP address from the superglobal $_SERVER.
900
     *
901
     * @param string $ipKey
902
     *
903
     * @throws InvalidArgumentException for IP key not set in the $_SERVER var
904
     *
905
     * @return static
906
     */
907
    public function setIpKey(string $ipKey) : static
908
    {
909
        if (!isset($_SERVER[$ipKey])) {
2✔
910
            throw new InvalidArgumentException(
1✔
911
                'The IP Key "' . $ipKey . '" is not set in the $_SERVER'
1✔
912
            );
1✔
913
        }
914
        $this->ipKey = $ipKey;
1✔
915
        return $this;
1✔
916
    }
917

918
    /**
919
     * Get the key used to get the IP address from the superglobal $_SERVER.
920
     *
921
     * @return string
922
     */
923
    public function getIpKey() : string
924
    {
925
        return $this->ipKey;
14✔
926
    }
927

928
    #[Override]
929
    #[Pure]
930
    public function getMethod() : string
931
    {
932
        return parent::getMethod();
31✔
933
    }
934

935
    /**
936
     * @param string $method
937
     *
938
     * @throws InvalidArgumentException for invalid method
939
     *
940
     * @return bool
941
     */
942
    #[Override]
943
    public function isMethod(string $method) : bool
944
    {
945
        return parent::isMethod($method);
22✔
946
    }
947

948
    /**
949
     * Gets data from the last request, if it was redirected.
950
     *
951
     * @param string|null $key a key name or null to get all data
952
     *
953
     * @see Response::redirect()
954
     *
955
     * @throws LogicException if PHP Session is not active to get redirect data
956
     *
957
     * @return mixed an array containing all data, the key value or null
958
     * if the key was not found
959
     */
960
    public function getRedirectData(?string $key = null) : mixed
961
    {
962
        static $data;
4✔
963
        if ($data === null && \session_status() !== \PHP_SESSION_ACTIVE) {
4✔
964
            throw new LogicException('Session must be active to get redirect data');
1✔
965
        }
966
        if ($data === null) {
3✔
967
            $data = $_SESSION['$']['redirect_data'] ?? false;
3✔
968
            unset($_SESSION['$']['redirect_data']);
3✔
969
        }
970
        if ($key !== null && $data) {
3✔
971
            return ArraySimple::value($key, $data);
1✔
972
        }
973
        return $data === false ? null : $data;
3✔
974
    }
975

976
    /**
977
     * Get the URL port.
978
     *
979
     * @return int
980
     */
981
    public function getPort() : int
982
    {
983
        return $this->port ?? $_SERVER['SERVER_PORT'];
2✔
984
    }
985

986
    /**
987
     * Get POST data.
988
     *
989
     * @param string|null $name
990
     * @param int|null $filter
991
     * @param array<int,int>|int $filterOptions
992
     *
993
     * @throws RequestParseBodyException
994
     *
995
     * @return mixed
996
     */
997
    public function getPost(
998
        ?string $name = null,
999
        ?int $filter = null,
1000
        array | int $filterOptions = 0
1001
    ) : mixed {
1002
        if ($this->isEnabledPostDataReading()) {
10✔
1003
            return $this->filterInput(\INPUT_POST, $name, $filter, $filterOptions);
10✔
1004
        }
UNCOV
1005
        if ($this->isMethod(Method::POST)) {
×
1006
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
×
1007
        }
UNCOV
1008
        return $name === null ? [] : null;
×
1009
    }
1010

1011
    /**
1012
     * Get PATCH data.
1013
     *
1014
     * @param string|null $name
1015
     * @param int|null $filter
1016
     * @param array<int,int>|int $filterOptions
1017
     *
1018
     * @throws RequestParseBodyException
1019
     *
1020
     * @return mixed
1021
     */
1022
    public function getPatch(
1023
        ?string $name = null,
1024
        ?int $filter = null,
1025
        array | int $filterOptions = 0
1026
    ) : mixed {
1027
        if ($this->isMethod(Method::PATCH)) {
1✔
1028
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
1029
        }
1030
        return $name === null ? [] : null;
1✔
1031
    }
1032

1033
    /**
1034
     * Get PUT data.
1035
     *
1036
     * @param string|null $name
1037
     * @param int|null $filter
1038
     * @param array<int,int>|int $filterOptions
1039
     *
1040
     * @throws RequestParseBodyException
1041
     *
1042
     * @return mixed
1043
     */
1044
    public function getPut(
1045
        ?string $name = null,
1046
        ?int $filter = null,
1047
        array | int $filterOptions = 0
1048
    ) : mixed {
1049
        if ($this->isMethod(Method::PUT)) {
1✔
1050
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
1051
        }
1052
        return $name === null ? [] : null;
1✔
1053
    }
1054

1055
    /**
1056
     * Get the Referer header.
1057
     *
1058
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
1059
     *
1060
     * @return URL|null
1061
     */
1062
    public function getReferer() : ?URL
1063
    {
1064
        if (!isset($this->referrer)) {
2✔
1065
            $this->referrer = false;
2✔
1066
            $referer = $_SERVER['HTTP_REFERER'] ?? null;
2✔
1067
            if ($referer !== null) {
2✔
1068
                try {
1069
                    $this->referrer = new URL($referer);
2✔
1070
                } catch (InvalidArgumentException) {
1✔
1071
                    $this->referrer = false;
1✔
1072
                }
1073
            }
1074
        }
1075
        return $this->referrer ?: null;
2✔
1076
    }
1077

1078
    /**
1079
     * Get $_SERVER variables.
1080
     *
1081
     * @param string|null $name
1082
     * @param int|null $filter
1083
     * @param array<int,int>|int $filterOptions
1084
     *
1085
     * @return mixed
1086
     */
1087
    public function getServer(
1088
        ?string $name = null,
1089
        ?int $filter = null,
1090
        array | int $filterOptions = 0
1091
    ) : mixed {
1092
        return $this->filterInput(\INPUT_SERVER, $name, $filter, $filterOptions);
12✔
1093
    }
1094

1095
    /**
1096
     * Gets the requested URL.
1097
     *
1098
     * @return URL
1099
     */
1100
    #[Override]
1101
    #[Pure]
1102
    public function getUrl() : URL
1103
    {
1104
        return parent::getUrl();
162✔
1105
    }
1106

1107
    /**
1108
     * Gets the User Agent client.
1109
     *
1110
     * @return UserAgent|null the UserAgent object or null if no user-agent
1111
     * header was received
1112
     */
1113
    public function getUserAgent() : ?UserAgent
1114
    {
1115
        if (isset($this->userAgent) && $this->userAgent instanceof UserAgent) {
13✔
1116
            return $this->userAgent;
1✔
1117
        }
1118
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
13✔
1119
        $userAgent ? $this->setUserAgent($userAgent) : $this->userAgent = false;
13✔
1120
        return $this->userAgent ?: null;
13✔
1121
    }
1122

1123
    /**
1124
     * @param UserAgent|string $userAgent
1125
     *
1126
     * @return static
1127
     */
1128
    protected function setUserAgent(UserAgent | string $userAgent) : static
1129
    {
1130
        if (!$userAgent instanceof UserAgent) {
4✔
1131
            $userAgent = new UserAgent($userAgent);
4✔
1132
        }
1133
        $this->userAgent = $userAgent;
4✔
1134
        return $this;
4✔
1135
    }
1136

1137
    /**
1138
     * Check if is an AJAX Request based in the X-Requested-With Header.
1139
     *
1140
     * The X-Requested-With Header containing the "XMLHttpRequest" value is
1141
     * used by various javascript libraries.
1142
     *
1143
     * @return bool
1144
     */
1145
    public function isAjax() : bool
1146
    {
1147
        if (isset($this->isAjax)) {
2✔
1148
            return $this->isAjax;
2✔
1149
        }
1150
        $received = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? null;
2✔
1151
        return $this->isAjax = ($received
2✔
1152
            && \strtolower($received) === 'xmlhttprequest');
2✔
1153
    }
1154

1155
    /**
1156
     * Say if a connection has HTTPS.
1157
     *
1158
     * @return bool
1159
     */
1160
    public function isSecure() : bool
1161
    {
1162
        if (isset($this->isSecure)) {
162✔
1163
            return $this->isSecure;
16✔
1164
        }
1165
        $scheme = $_SERVER['REQUEST_SCHEME'] ?? null;
162✔
1166
        $https = $_SERVER['HTTPS'] ?? null;
162✔
1167
        return $this->isSecure = ($scheme === 'https' || $https === 'on');
162✔
1168
    }
1169

1170
    /**
1171
     * Tells if the Content-Type header is application/x-www-form-urlencoded.
1172
     *
1173
     * @return bool
1174
     */
1175
    public function isFormUrlEncoded() : bool
1176
    {
1177
        return $this->parseContentType() === 'application/x-www-form-urlencoded';
17✔
1178
    }
1179

1180
    /**
1181
     * Tells if the Content-Type header is multipart/form-data.
1182
     *
1183
     * @return bool
1184
     */
1185
    public function isFormData() : bool
1186
    {
1187
        return $this->parseContentType() === 'multipart/form-data';
5✔
1188
    }
1189

1190
    /**
1191
     * Tells if the Content-Type header is application/x-www-form-urlencoded or
1192
     * multipart/form-data.
1193
     *
1194
     * @return bool
1195
     */
1196
    public function isForm() : bool
1197
    {
1198
        return $this->isFormUrlEncoded() || $this->isFormData();
6✔
1199
    }
1200

1201
    /**
1202
     * Say if the request is a JSON call.
1203
     *
1204
     * @return bool
1205
     */
1206
    public function isJson() : bool
1207
    {
1208
        return $this->parseContentType() === 'application/json';
1✔
1209
    }
1210

1211
    /**
1212
     * Say if the request method is POST.
1213
     *
1214
     * @return bool
1215
     */
1216
    public function isPost() : bool
1217
    {
1218
        return $this->isMethod(Method::POST);
13✔
1219
    }
1220

1221
    /**
1222
     * @see https://php.watch/codex/enable_post_data_reading
1223
     *
1224
     * @return bool
1225
     */
1226
    protected function isEnabledPostDataReading() : bool
1227
    {
1228
        return \ini_get('enable_post_data_reading') === '1';
23✔
1229
    }
1230

1231
    /**
1232
     * @see https://www.sitepoint.com/community/t/-files-array-structure/2728/5
1233
     *
1234
     * @return array<string,UploadedFile|array<mixed>>
1235
     */
1236
    protected function getInputFiles() : array
1237
    {
1238
        if (!$this->isParsedBody() && !$this->isEnabledPostDataReading()) {
16✔
UNCOV
1239
            $this->parseBody($this->getParseBodyOptions());
×
1240
        }
1241
        $files = $this->parsedBody[1] ?? $_FILES;
16✔
1242
        if (empty($files)) {
16✔
1243
            return [];
13✔
1244
        }
1245
        $makeObjects = static function (
3✔
1246
            array $array,
3✔
1247
            callable $makeObjects
3✔
1248
        ) : UploadedFile | array {
3✔
1249
            $return = [];
3✔
1250
            foreach ($array as $k => $v) {
3✔
1251
                if (\is_array($v)) {
3✔
1252
                    $return[$k] = $makeObjects($v, $makeObjects);
3✔
1253
                    continue;
3✔
1254
                }
1255
                return new UploadedFile($array);
3✔
1256
            }
1257
            return $return;
3✔
1258
        };
3✔
1259
        return $makeObjects(ArraySimple::files($files), $makeObjects); // @phpstan-ignore-line
3✔
1260
    }
1261

1262
    /**
1263
     * @param string $host
1264
     *
1265
     * @throws InvalidArgumentException for invalid host
1266
     *
1267
     * @return static
1268
     */
1269
    protected function setHost(string $host) : static
1270
    {
1271
        $filteredHost = 'http://' . $host;
162✔
1272
        $filteredHost = \filter_var($filteredHost, \FILTER_VALIDATE_URL);
162✔
1273
        if (!$filteredHost) {
162✔
1274
            throw new InvalidArgumentException("Invalid host: {$host}");
1✔
1275
        }
1276
        $host = \parse_url($filteredHost);
162✔
1277
        $this->host = $host['host']; // @phpstan-ignore-line
162✔
1278
        if (isset($host['port'])) {
162✔
1279
            $this->port = $host['port'];
9✔
1280
        }
1281
        return $this;
162✔
1282
    }
1283

1284
    /**
1285
     * Make a Response with the current Request.
1286
     *
1287
     * @return Response
1288
     */
1289
    public function makeResponse() : Response
1290
    {
1291
        return new Response($this);
1✔
1292
    }
1293
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc