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

aplus-framework / http / 22592221798

02 Mar 2026 07:26PM UTC coverage: 98.97% (+0.001%) from 98.969%
22592221798

push

github

natanfelles
Update logic in a ternary operator

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

4 existing lines in 1 file now uncovered.

1633 of 1650 relevant lines covered (98.97%)

14.59 hits per line

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

98.82
/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
     * @var array<string,array<mixed>|null>
58
     */
59
    protected array $negotiation = [
60
        'ACCEPT' => null,
61
        'CHARSET' => null,
62
        'ENCODING' => null,
63
        'LANGUAGE' => null,
64
    ];
65
    protected URL | false | null $referrer = null;
66
    protected UserAgent | false | null $userAgent = null;
67
    protected bool $isAjax;
68
    /**
69
     * Tell if is an HTTPS connection.
70
     *
71
     * @var bool
72
     */
73
    protected bool $isSecure;
74
    protected int $jsonFlags = 0;
75
    protected string $ipKey = 'REMOTE_ADDR';
76

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

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

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

121
    /**
122
     * NOTE: Created by comparing a request made with Firefox.
123
     *
124
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data
125
     *
126
     * @return string
127
     */
128
    protected function getMultipartBody() : string
129
    {
130
        $bodyParts = [];
1✔
131
        /**
132
         * @var array<string,string> $parsedBody
133
         */
134
        $parsedBody = ArraySimple::convert($this->getParsedBody());
1✔
135
        foreach ($parsedBody as $field => $value) {
1✔
136
            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
1✔
137
            $bodyParts[] = \implode("\r\n", [
1✔
138
                "Content-Disposition: form-data; name=\"{$field}\"",
1✔
139
                '',
1✔
140
                $value,
1✔
141
            ]);
1✔
142
        }
143
        /**
144
         * @var array<string,UploadedFile> $files
145
         */
146
        $files = ArraySimple::convert($this->getFiles());
1✔
147
        foreach ($files as $field => $file) {
1✔
148
            $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5);
1✔
149
            $field = \preg_replace('/\[\d+]/', '[]', $field);
1✔
150
            $filename = \htmlspecialchars($file->getFullPath(), \ENT_QUOTES | \ENT_HTML5);
1✔
151
            $contentType = $file->getClientType() ?: 'application/octet-stream';
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: ' . $contentType,
1✔
160
                '',
1✔
161
                $data,
1✔
162
            ]);
1✔
163
        }
164
        $boundary = '';
1✔
165
        $boundaryParts = \explode(';', $this->getContentType());
1✔
166
        foreach ($boundaryParts as $boundaryPart) {
1✔
167
            $boundaryPart = \trim($boundaryPart);
1✔
168
            if (\str_starts_with(\strtolower($boundaryPart), 'boundary=')) {
1✔
169
                $boundary = \substr($boundaryPart, \strlen('boundary='));
1✔
170
                break;
1✔
171
            }
172
        }
173
        foreach ($bodyParts as &$part) {
1✔
174
            $part = "--{$boundary}\r\n{$part}";
1✔
175
        }
176
        unset($part);
1✔
177
        $bodyParts[] = "--{$boundary}--";
1✔
178
        $bodyParts[] = '';
1✔
179
        $bodyParts = \implode("\r\n", $bodyParts);
1✔
180
        /**
181
         * Uncomment the code below to make a raw test.
182
         *
183
         * @see \Tests\HTTP\RequestTest::testToStringMultipart()
184
         */
185
        /*
186
        $serverLength = (string) $_SERVER['CONTENT_LENGTH'];
187
        $algoLength = (string) \strlen($bodyParts);
188
        \var_dump($serverLength, $algoLength);
189
        if ($serverLength !== $algoLength) {
190
            throw new \Exception(
191
                '$_SERVER CONTENT_LENGTH is ' . $serverLength
192
                . ', but the algorithm calculated ' . $algoLength
193
            );
194
        }
195
        */
196
        return $bodyParts;
1✔
197
    }
198

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

215
    protected function prepareStatusLine() : void
216
    {
217
        $this->setProtocol($_SERVER['SERVER_PROTOCOL']);
165✔
218
        $this->setMethod($_SERVER['REQUEST_METHOD']);
165✔
219
        $url = $this->isSecure() ? 'https' : 'http';
165✔
220
        $url .= '://' . $_SERVER['HTTP_HOST'];
165✔
221
        $url .= $_SERVER['REQUEST_URI'];
165✔
222
        $this->setUrl($url);
165✔
223
        $this->setHost($this->getUrl()->getHost());
165✔
224
    }
225

226
    public function getHeader(string $name) : ?string
227
    {
228
        $this->prepareHeaders();
40✔
229
        return $this->headers[\strtolower($name)] ?? null;
40✔
230
    }
231

232
    /**
233
     * @return array<string,string>
234
     */
235
    public function getHeaders() : array
236
    {
237
        $this->prepareHeaders();
15✔
238
        return $this->headers;
15✔
239
    }
240

241
    protected function prepareHeaders() : void
242
    {
243
        if (!empty($this->headers)) {
42✔
244
            return;
35✔
245
        }
246
        foreach ($_SERVER as $name => $value) {
28✔
247
            if (\str_starts_with($name, 'HTTP_')) {
28✔
248
                $name = \strtr(\substr($name, 5), ['_' => '-']);
28✔
249
                $this->setHeader($name, $value);
28✔
250
            }
251
        }
252
    }
253

254
    public function getCookie(string $name) : ?Cookie
255
    {
256
        $this->prepareCookies();
1✔
257
        return $this->cookies[$name] ?? null;
1✔
258
    }
259

260
    /**
261
     * Get all Cookies.
262
     *
263
     * @return array<string,Cookie>
264
     */
265
    public function getCookies() : array
266
    {
267
        $this->prepareCookies();
1✔
268
        return $this->cookies;
1✔
269
    }
270

271
    protected function prepareCookies() : void
272
    {
273
        if (!empty($this->cookies)) {
2✔
274
            return;
1✔
275
        }
276
        foreach ($_COOKIE as $name => $value) {
2✔
277
            $this->setCookie(new Cookie($name, $value));
2✔
278
        }
279
    }
280

281
    /**
282
     * @see https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input
283
     *
284
     * @return string
285
     */
286
    public function getBody() : string
287
    {
288
        if (!isset($this->body)) {
18✔
289
            $this->body = (string) \file_get_contents('php://input');
14✔
290
        }
291
        return $this->body;
18✔
292
    }
293

294
    protected function prepareFiles() : void
295
    {
296
        if (isset($this->files)) {
16✔
297
            return;
13✔
298
        }
299
        $this->files = $this->getInputFiles();
16✔
300
    }
301

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

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

357
    /**
358
     * Get the Authorization type.
359
     *
360
     * @return string|null Basic, Bearer, Digest or null for none
361
     */
362
    public function getAuthType() : ?string
363
    {
364
        if ($this->authType === null) {
4✔
365
            $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
4✔
366
            if ($auth) {
4✔
367
                $this->parseAuth($auth);
3✔
368
            }
369
        }
370
        return $this->authType;
4✔
371
    }
372

373
    /**
374
     * Get Basic authorization.
375
     *
376
     * @return array<string>|null Two keys: username and password
377
     */
378
    #[ArrayShape(['username' => 'string|null', 'password' => 'string|null'])]
379
    public function getBasicAuth() : ?array
380
    {
381
        return $this->getAuthType() === 'Basic'
2✔
382
            ? $this->auth
1✔
383
            : null;
2✔
384
    }
385

386
    /**
387
     * Get Bearer authorization.
388
     *
389
     * @return array<string>|null One key: token
390
     */
391
    #[ArrayShape(['token' => 'string|null'])]
392
    public function getBearerAuth() : ?array
393
    {
394
        return $this->getAuthType() === 'Bearer'
2✔
395
            ? $this->auth
1✔
396
            : null;
2✔
397
    }
398

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

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

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

468
    /**
469
     * @param string $attributes
470
     *
471
     * @return array<string,string|null>
472
     */
473
    #[ArrayShape(['token' => 'string|null'])]
474
    #[Pure]
475
    protected function parseBearerAuth(string $attributes) : array
476
    {
477
        $data = [
1✔
478
            'token' => null,
1✔
479
        ];
1✔
480
        if ($attributes) {
1✔
481
            $data['token'] = $attributes;
1✔
482
        }
483
        return $data;
1✔
484
    }
485

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

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

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

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

598
    /**
599
     * @param array<string,mixed>|null $options
600
     *
601
     * @throws RequestParseBodyException
602
     *
603
     * @return array<int,array<mixed>>
604
     */
605
    protected function requestParseBody(?array $options = null) : array
606
    {
607
        return \request_parse_body($options);
1✔
608
    }
609

610
    public function isParsedBody() : bool
611
    {
612
        return isset($this->parsedBody);
21✔
613
    }
614

615
    /**
616
     * @return array<string,mixed>|null
617
     */
618
    public function getParseBodyOptions() : ?array
619
    {
620
        return $this->parseBodyOptions;
4✔
621
    }
622

623
    /**
624
     * @param array<string,mixed>|null $options
625
     *
626
     * @see https://www.php.net/manual/en/function.request-parse-body.php#refsect1-function.request-parse-body-parameters
627
     */
628
    public function setParseBodyOptions(?array $options) : static
629
    {
630
        $this->parseBodyOptions = $options;
1✔
631
        return $this;
1✔
632
    }
633

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

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

692
    /**
693
     * @param string $type
694
     * @param array<int,string> $negotiable
695
     *
696
     * @return string
697
     */
698
    protected function negotiate(string $type, array $negotiable) : string
699
    {
700
        $negotiable = \array_map('\strtolower', $negotiable);
4✔
701
        foreach ($this->getNegotiableValues($type) as $item) {
4✔
702
            if (\in_array($item, $negotiable, true)) {
4✔
703
                return $item;
4✔
704
            }
705
        }
706
        return $negotiable[0];
4✔
707
    }
708

709
    /**
710
     * Get the mime types of the Accept header.
711
     *
712
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
713
     *
714
     * @return array<int,string>
715
     */
716
    public function getAccepts() : array
717
    {
718
        return $this->getNegotiableValues('ACCEPT');
2✔
719
    }
720

721
    /**
722
     * Negotiate the Accept header.
723
     *
724
     * @param array<int,string> $negotiable Allowed mime types
725
     *
726
     * @return string The negotiated mime type
727
     */
728
    public function negotiateAccept(array $negotiable) : string
729
    {
730
        return $this->negotiate('ACCEPT', $negotiable);
1✔
731
    }
732

733
    /**
734
     * Get the Accept-Charset's.
735
     *
736
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
737
     *
738
     * @return array<int,string>
739
     */
740
    public function getCharsets() : array
741
    {
742
        return $this->getNegotiableValues('CHARSET');
1✔
743
    }
744

745
    /**
746
     * Negotiate the Accept-Charset.
747
     *
748
     * @param array<int,string> $negotiable Allowed charsets
749
     *
750
     * @return string The negotiated charset
751
     */
752
    public function negotiateCharset(array $negotiable) : string
753
    {
754
        return $this->negotiate('CHARSET', $negotiable);
1✔
755
    }
756

757
    /**
758
     * Get the Accept-Encoding.
759
     *
760
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
761
     *
762
     * @return array<int,string>
763
     */
764
    public function getEncodings() : array
765
    {
766
        return $this->getNegotiableValues('ENCODING');
1✔
767
    }
768

769
    /**
770
     * Negotiate the Accept-Encoding.
771
     *
772
     * @param array<int,string> $negotiable The allowed encodings
773
     *
774
     * @return string The negotiated encoding
775
     */
776
    public function negotiateEncoding(array $negotiable) : string
777
    {
778
        return $this->negotiate('ENCODING', $negotiable);
1✔
779
    }
780

781
    /**
782
     * Get the Accept-Language's.
783
     *
784
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
785
     *
786
     * @return array<int,string>
787
     */
788
    public function getLanguages() : array
789
    {
790
        return $this->getNegotiableValues('LANGUAGE');
1✔
791
    }
792

793
    /**
794
     * Negotiated the Accept-Language.
795
     *
796
     * @param array<int,string> $negotiable Allowed languages
797
     *
798
     * @return string The negotiated language
799
     */
800
    public function negotiateLanguage(array $negotiable) : string
801
    {
802
        return $this->negotiate('LANGUAGE', $negotiable);
1✔
803
    }
804

805
    /**
806
     * Get the Content-Type header value.
807
     *
808
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
809
     *
810
     * @return string|null
811
     */
812
    #[Pure]
813
    public function getContentType() : ?string
814
    {
815
        return $_SERVER['HTTP_CONTENT_TYPE'] ?? null;
2✔
816
    }
817

818
    /**
819
     * @param string|null $name
820
     * @param int|null $filter
821
     * @param array<int,int>|int $filterOptions
822
     *
823
     * @return mixed
824
     */
825
    public function getEnv(
826
        ?string $name = null,
827
        ?int $filter = null,
828
        array | int $filterOptions = 0
829
    ) : mixed {
830
        return $this->filterInput(\INPUT_ENV, $name, $filter, $filterOptions);
1✔
831
    }
832

833
    /**
834
     * @return array<string,UploadedFile|array<mixed>>
835
     */
836
    public function getFiles() : array
837
    {
838
        $this->prepareFiles();
4✔
839
        return $this->files;
4✔
840
    }
841

842
    public function hasFiles() : bool
843
    {
844
        $this->prepareFiles();
13✔
845
        return !empty($this->files);
13✔
846
    }
847

848
    public function getFile(string $name) : ?UploadedFile
849
    {
850
        $this->prepareFiles();
1✔
851
        $file = ArraySimple::value($name, $this->files);
1✔
852
        return \is_array($file) ? null : $file;
1✔
853
    }
854

855
    /**
856
     * Get the URL GET queries.
857
     *
858
     * @param string|null $name
859
     * @param int|null $filter
860
     * @param array<int,int>|int $filterOptions
861
     *
862
     * @return mixed
863
     */
864
    public function getGet(
865
        ?string $name = null,
866
        ?int $filter = null,
867
        array | int $filterOptions = 0
868
    ) : mixed {
869
        return $this->filterInput(\INPUT_GET, $name, $filter, $filterOptions);
1✔
870
    }
871

872
    /**
873
     * @return string
874
     */
875
    #[Pure]
876
    public function getHost() : string
877
    {
878
        return $this->host;
2✔
879
    }
880

881
    /**
882
     * Get the X-Request-ID header.
883
     *
884
     * @return string|null
885
     */
886
    public function getId() : ?string
887
    {
888
        return $_SERVER['HTTP_X_REQUEST_ID'] ?? null;
2✔
889
    }
890

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

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

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

931
    #[Override]
932
    #[Pure]
933
    public function getMethod() : string
934
    {
935
        return parent::getMethod();
31✔
936
    }
937

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

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

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

989
    /**
990
     * Make an empty value according to the name type.
991
     *
992
     * @param string|null $name
993
     *
994
     * @return array<mixed>|null
995
     */
996
    protected function makeEmptyValue(?string $name) : ?array
997
    {
998
        return $name === null ? [] : null;
2✔
999
    }
1000

1001
    /**
1002
     * Get POST data.
1003
     *
1004
     * @param string|null $name
1005
     * @param int|null $filter
1006
     * @param array<int,int>|int $filterOptions
1007
     *
1008
     * @throws RequestParseBodyException
1009
     *
1010
     * @return mixed
1011
     */
1012
    public function getPost(
1013
        ?string $name = null,
1014
        ?int $filter = null,
1015
        array | int $filterOptions = 0
1016
    ) : mixed {
1017
        if ($this->isEnabledPostDataReading()) {
10✔
1018
            return $this->filterInput(\INPUT_POST, $name, $filter, $filterOptions);
10✔
1019
        }
UNCOV
1020
        if ($this->isMethod(Method::POST)) {
×
UNCOV
1021
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
×
1022
        }
UNCOV
1023
        return $this->makeEmptyValue($name);
×
1024
    }
1025

1026
    /**
1027
     * Get PATCH data.
1028
     *
1029
     * @param string|null $name
1030
     * @param int|null $filter
1031
     * @param array<int,int>|int $filterOptions
1032
     *
1033
     * @throws RequestParseBodyException
1034
     *
1035
     * @return mixed
1036
     */
1037
    public function getPatch(
1038
        ?string $name = null,
1039
        ?int $filter = null,
1040
        array | int $filterOptions = 0
1041
    ) : mixed {
1042
        if ($this->isMethod(Method::PATCH)) {
1✔
1043
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
1044
        }
1045
        return $this->makeEmptyValue($name);
1✔
1046
    }
1047

1048
    /**
1049
     * Get PUT data.
1050
     *
1051
     * @param string|null $name
1052
     * @param int|null $filter
1053
     * @param array<int,int>|int $filterOptions
1054
     *
1055
     * @throws RequestParseBodyException
1056
     *
1057
     * @return mixed
1058
     */
1059
    public function getPut(
1060
        ?string $name = null,
1061
        ?int $filter = null,
1062
        array | int $filterOptions = 0
1063
    ) : mixed {
1064
        if ($this->isMethod(Method::PUT)) {
1✔
1065
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
1066
        }
1067
        return $this->makeEmptyValue($name);
1✔
1068
    }
1069

1070
    /**
1071
     * Get the Referer header.
1072
     *
1073
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
1074
     *
1075
     * @return URL|null
1076
     */
1077
    public function getReferer() : ?URL
1078
    {
1079
        if ($this->referrer instanceof URL) {
2✔
1080
            return $this->referrer;
1✔
1081
        }
1082
        if ($this->referrer === false) {
2✔
1083
            return null;
2✔
1084
        }
1085
        $referer = $_SERVER['HTTP_REFERER'] ?? null;
2✔
1086
        if ($referer === null) {
2✔
1087
            $this->referrer = false;
1✔
1088
            return null;
1✔
1089
        }
1090
        try {
1091
            $this->referrer = new URL($referer);
2✔
1092
        } catch (InvalidArgumentException) {
2✔
1093
            $this->referrer = false;
2✔
1094
        }
1095
        return $this->getReferer();
2✔
1096
    }
1097

1098
    /**
1099
     * Get $_SERVER variables.
1100
     *
1101
     * @param string|null $name
1102
     * @param int|null $filter
1103
     * @param array<int,int>|int $filterOptions
1104
     *
1105
     * @return mixed
1106
     */
1107
    public function getServer(
1108
        ?string $name = null,
1109
        ?int $filter = null,
1110
        array | int $filterOptions = 0
1111
    ) : mixed {
1112
        return $this->filterInput(\INPUT_SERVER, $name, $filter, $filterOptions);
12✔
1113
    }
1114

1115
    /**
1116
     * Gets the requested URL.
1117
     *
1118
     * @return URL
1119
     */
1120
    #[Override]
1121
    #[Pure]
1122
    public function getUrl() : URL
1123
    {
1124
        return parent::getUrl();
165✔
1125
    }
1126

1127
    /**
1128
     * Gets the User Agent client.
1129
     *
1130
     * @return UserAgent|null the UserAgent object or null if no user-agent
1131
     * header was received
1132
     */
1133
    public function getUserAgent() : ?UserAgent
1134
    {
1135
        if ($this->userAgent instanceof UserAgent) {
13✔
1136
            return $this->userAgent;
4✔
1137
        }
1138
        if ($this->userAgent === false) {
13✔
1139
            return null;
10✔
1140
        }
1141
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
13✔
1142
        isset($userAgent)
13✔
1143
            ? $this->setUserAgent($userAgent)
4✔
1144
            : $this->userAgent = false;
10✔
1145
        return $this->getUserAgent();
13✔
1146
    }
1147

1148
    /**
1149
     * @param UserAgent|string $userAgent
1150
     *
1151
     * @return static
1152
     */
1153
    protected function setUserAgent(UserAgent | string $userAgent) : static
1154
    {
1155
        if (!$userAgent instanceof UserAgent) {
4✔
1156
            $userAgent = new UserAgent($userAgent);
4✔
1157
        }
1158
        $this->userAgent = $userAgent;
4✔
1159
        return $this;
4✔
1160
    }
1161

1162
    /**
1163
     * Check if is an AJAX Request based in the X-Requested-With Header.
1164
     *
1165
     * The X-Requested-With Header containing the "XMLHttpRequest" value is
1166
     * used by various JavaScript libraries.
1167
     *
1168
     * @return bool
1169
     */
1170
    public function isAjax() : bool
1171
    {
1172
        if (isset($this->isAjax)) {
2✔
1173
            return $this->isAjax;
2✔
1174
        }
1175
        $received = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? null;
2✔
1176
        return $this->isAjax = ($received
2✔
1177
            && \strtolower($received) === 'xmlhttprequest');
2✔
1178
    }
1179

1180
    /**
1181
     * Say if a connection has HTTPS.
1182
     *
1183
     * @return bool
1184
     */
1185
    public function isSecure() : bool
1186
    {
1187
        if (isset($this->isSecure)) {
165✔
1188
            return $this->isSecure;
16✔
1189
        }
1190
        $scheme = $_SERVER['REQUEST_SCHEME'] ?? '';
165✔
1191
        $https = $_SERVER['HTTPS'] ?? '';
165✔
1192
        return $this->isSecure = (
165✔
1193
            \strtolower($scheme) === 'https' || \strtolower($https) === 'on'
165✔
1194
        );
165✔
1195
    }
1196

1197
    /**
1198
     * Tells if the Content-Type header is application/x-www-form-urlencoded.
1199
     *
1200
     * @return bool
1201
     */
1202
    public function isFormUrlEncoded() : bool
1203
    {
1204
        return $this->parseContentType() === 'application/x-www-form-urlencoded';
17✔
1205
    }
1206

1207
    /**
1208
     * Tells if the Content-Type header is multipart/form-data.
1209
     *
1210
     * @return bool
1211
     */
1212
    public function isFormData() : bool
1213
    {
1214
        return $this->parseContentType() === 'multipart/form-data';
5✔
1215
    }
1216

1217
    /**
1218
     * Tells if the Content-Type header is application/x-www-form-urlencoded or
1219
     * multipart/form-data.
1220
     *
1221
     * @return bool
1222
     */
1223
    public function isForm() : bool
1224
    {
1225
        return $this->isFormUrlEncoded() || $this->isFormData();
6✔
1226
    }
1227

1228
    /**
1229
     * Say if the request is a JSON call.
1230
     *
1231
     * @return bool
1232
     */
1233
    public function isJson() : bool
1234
    {
1235
        return $this->parseContentType() === 'application/json';
1✔
1236
    }
1237

1238
    /**
1239
     * Say if the request method is POST.
1240
     *
1241
     * @return bool
1242
     */
1243
    public function isPost() : bool
1244
    {
1245
        return $this->isMethod(Method::POST);
13✔
1246
    }
1247

1248
    /**
1249
     * @see https://php.watch/codex/enable_post_data_reading
1250
     *
1251
     * @return bool
1252
     */
1253
    protected function isEnabledPostDataReading() : bool
1254
    {
1255
        return \ini_get('enable_post_data_reading') === '1';
23✔
1256
    }
1257

1258
    /**
1259
     * @see https://www.sitepoint.com/community/t/-files-array-structure/2728/5
1260
     *
1261
     * @return array<string,UploadedFile|array<mixed>>
1262
     */
1263
    protected function getInputFiles() : array
1264
    {
1265
        if (!$this->isParsedBody() && !$this->isEnabledPostDataReading()) {
16✔
UNCOV
1266
            $this->parseBody($this->getParseBodyOptions());
×
1267
        }
1268
        $files = $this->parsedBody[1] ?? $_FILES;
16✔
1269
        if (empty($files)) {
16✔
1270
            return [];
13✔
1271
        }
1272
        $makeObjects = static function (
3✔
1273
            array $array,
3✔
1274
            callable $makeObjects
3✔
1275
        ) : UploadedFile | array {
3✔
1276
            $return = [];
3✔
1277
            foreach ($array as $k => $v) {
3✔
1278
                if (\is_array($v)) {
3✔
1279
                    $return[$k] = $makeObjects($v, $makeObjects);
3✔
1280
                    continue;
3✔
1281
                }
1282
                return new UploadedFile($array);
3✔
1283
            }
1284
            return $return;
3✔
1285
        };
3✔
1286
        return $makeObjects(ArraySimple::files($files), $makeObjects); // @phpstan-ignore-line
3✔
1287
    }
1288

1289
    /**
1290
     * @param string $host
1291
     *
1292
     * @throws InvalidArgumentException for invalid host
1293
     *
1294
     * @return static
1295
     */
1296
    protected function setHost(string $host) : static
1297
    {
1298
        $filteredHost = 'http://' . $host;
165✔
1299
        $filteredHost = \filter_var($filteredHost, \FILTER_VALIDATE_URL);
165✔
1300
        if (!$filteredHost) {
165✔
1301
            throw new InvalidArgumentException("Invalid host: {$host}");
1✔
1302
        }
1303
        $host = \parse_url($filteredHost);
165✔
1304
        $this->host = $host['host']; // @phpstan-ignore-line
165✔
1305
        if (isset($host['port'])) {
165✔
1306
            $this->port = $host['port'];
9✔
1307
        }
1308
        return $this;
165✔
1309
    }
1310

1311
    /**
1312
     * Make a Response with the current Request.
1313
     *
1314
     * @return Response
1315
     */
1316
    public function makeResponse() : Response
1317
    {
1318
        return new Response($this);
1✔
1319
    }
1320
}
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