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

aplus-framework / http / 23963185908

03 Apr 2026 09:38PM UTC coverage: 98.973% (+0.003%) from 98.97%
23963185908

push

github

natanfelles
Add code completion to the Request::setIpKey method

1638 of 1655 relevant lines covered (98.97%)

14.56 hits per line

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

98.83
/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
        $key = $this->getIpKey();
14✔
899
        if ($key === 'HTTP_X_FORWARDED_FOR') {
14✔
900
            $ip = \explode(',', $_SERVER[$key], 2)[0];
1✔
901
            $ip = \trim($ip);
1✔
902
            return $ip;
1✔
903
        }
904
        return $_SERVER[$key];
13✔
905
    }
906

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

927
    /**
928
     * Get the key used to get the IP address from the superglobal $_SERVER.
929
     *
930
     * @return string
931
     */
932
    public function getIpKey() : string
933
    {
934
        return $this->ipKey;
14✔
935
    }
936

937
    #[Override]
938
    #[Pure]
939
    public function getMethod() : string
940
    {
941
        return parent::getMethod();
31✔
942
    }
943

944
    /**
945
     * @param string $method
946
     *
947
     * @throws InvalidArgumentException for invalid method
948
     *
949
     * @return bool
950
     */
951
    #[Override]
952
    public function isMethod(string $method) : bool
953
    {
954
        return parent::isMethod($method);
23✔
955
    }
956

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

985
    /**
986
     * Get the URL port.
987
     *
988
     * @return int
989
     */
990
    public function getPort() : int
991
    {
992
        return $this->port ?? $_SERVER['SERVER_PORT'];
2✔
993
    }
994

995
    /**
996
     * Make an empty value according to the name type.
997
     *
998
     * @param string|null $name
999
     *
1000
     * @return array<mixed>|null
1001
     */
1002
    protected function makeEmptyValue(?string $name) : ?array
1003
    {
1004
        return $name === null ? [] : null;
2✔
1005
    }
1006

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

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

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

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

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

1121
    /**
1122
     * Gets the requested URL.
1123
     *
1124
     * @return URL
1125
     */
1126
    #[Override]
1127
    #[Pure]
1128
    public function getUrl() : URL
1129
    {
1130
        return parent::getUrl();
165✔
1131
    }
1132

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

1154
    /**
1155
     * @param UserAgent|string $userAgent
1156
     *
1157
     * @return static
1158
     */
1159
    protected function setUserAgent(UserAgent | string $userAgent) : static
1160
    {
1161
        if (!$userAgent instanceof UserAgent) {
4✔
1162
            $userAgent = new UserAgent($userAgent);
4✔
1163
        }
1164
        $this->userAgent = $userAgent;
4✔
1165
        return $this;
4✔
1166
    }
1167

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

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

1203
    /**
1204
     * Tells if the Content-Type header is application/x-www-form-urlencoded.
1205
     *
1206
     * @return bool
1207
     */
1208
    public function isFormUrlEncoded() : bool
1209
    {
1210
        return $this->parseContentType() === 'application/x-www-form-urlencoded';
17✔
1211
    }
1212

1213
    /**
1214
     * Tells if the Content-Type header is multipart/form-data.
1215
     *
1216
     * @return bool
1217
     */
1218
    public function isFormData() : bool
1219
    {
1220
        return $this->parseContentType() === 'multipart/form-data';
5✔
1221
    }
1222

1223
    /**
1224
     * Tells if the Content-Type header is application/x-www-form-urlencoded or
1225
     * multipart/form-data.
1226
     *
1227
     * @return bool
1228
     */
1229
    public function isForm() : bool
1230
    {
1231
        return $this->isFormUrlEncoded() || $this->isFormData();
6✔
1232
    }
1233

1234
    /**
1235
     * Say if the request is a JSON call.
1236
     *
1237
     * @return bool
1238
     */
1239
    public function isJson() : bool
1240
    {
1241
        return $this->parseContentType() === 'application/json';
1✔
1242
    }
1243

1244
    /**
1245
     * Say if the request method is POST.
1246
     *
1247
     * @return bool
1248
     */
1249
    public function isPost() : bool
1250
    {
1251
        return $this->isMethod(Method::POST);
13✔
1252
    }
1253

1254
    /**
1255
     * @see https://php.watch/codex/enable_post_data_reading
1256
     *
1257
     * @return bool
1258
     */
1259
    protected function isEnabledPostDataReading() : bool
1260
    {
1261
        return \ini_get('enable_post_data_reading') === '1';
23✔
1262
    }
1263

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

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

1317
    /**
1318
     * Make a Response with the current Request.
1319
     *
1320
     * @return Response
1321
     */
1322
    public function makeResponse() : Response
1323
    {
1324
        return new Response($this);
1✔
1325
    }
1326
}
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