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

aplus-framework / http / 20939401720

13 Jan 2026 12:00AM UTC coverage: 98.771% (-0.003%) from 98.774%
20939401720

push

github

natanfelles
Fix: Use getParsedBody to create fields with multiple HTTP methods

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

4 existing lines in 1 file now uncovered.

1608 of 1628 relevant lines covered (98.77%)

14.22 hits per line

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

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

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

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

215
    public function getHeader(string $name) : ?string
216
    {
217
        $this->prepareHeaders();
39✔
218
        return $this->headers[\strtolower($name)] ?? null;
39✔
219
    }
220

221
    /**
222
     * @return array<string,string>
223
     */
224
    public function getHeaders() : array
225
    {
226
        $this->prepareHeaders();
15✔
227
        return $this->headers;
15✔
228
    }
229

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

243
    public function getCookie(string $name) : ?Cookie
244
    {
245
        $this->prepareCookies();
1✔
246
        return $this->cookies[$name] ?? null;
1✔
247
    }
248

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

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

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

283
    protected function prepareFiles() : void
284
    {
285
        if (isset($this->files)) {
16✔
286
            return;
13✔
287
        }
288
        $this->files = $this->getInputFiles();
16✔
289
    }
290

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

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

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

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

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

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

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

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

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

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

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

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

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

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

599
    public function isParsedBody() : bool
600
    {
601
        return isset($this->parsedBody);
21✔
602
    }
603

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

831
    public function hasFiles() : bool
832
    {
833
        $this->prepareFiles();
13✔
834
        return !empty($this->files);
13✔
835
    }
836

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

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

861
    /**
862
     * @return string
863
     */
864
    #[Pure]
865
    public function getHost() : string
866
    {
867
        return $this->host;
2✔
868
    }
869

870
    /**
871
     * Get the X-Request-ID header.
872
     *
873
     * @return string|null
874
     */
875
    public function getId() : ?string
876
    {
877
        return $_SERVER['HTTP_X_REQUEST_ID'] ?? null;
2✔
878
    }
879

880
    /**
881
     * Get the connection IP.
882
     *
883
     * @return string
884
     */
885
    public function getIp() : string
886
    {
887
        return $_SERVER[$this->getIpKey()];
14✔
888
    }
889

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

910
    /**
911
     * Get the key used to get the IP address from the superglobal $_SERVER.
912
     *
913
     * @return string
914
     */
915
    public function getIpKey() : string
916
    {
917
        return $this->ipKey;
14✔
918
    }
919

920
    #[Override]
921
    #[Pure]
922
    public function getMethod() : string
923
    {
924
        return parent::getMethod();
31✔
925
    }
926

927
    /**
928
     * @param string $method
929
     *
930
     * @throws InvalidArgumentException for invalid method
931
     *
932
     * @return bool
933
     */
934
    #[Override]
935
    public function isMethod(string $method) : bool
936
    {
937
        return parent::isMethod($method);
23✔
938
    }
939

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

968
    /**
969
     * Get the URL port.
970
     *
971
     * @return int
972
     */
973
    public function getPort() : int
974
    {
975
        return $this->port ?? $_SERVER['SERVER_PORT'];
2✔
976
    }
977

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

1003
    /**
1004
     * Get PATCH data.
1005
     *
1006
     * @param string|null $name
1007
     * @param int|null $filter
1008
     * @param array<int,int>|int $filterOptions
1009
     *
1010
     * @throws RequestParseBodyException
1011
     *
1012
     * @return mixed
1013
     */
1014
    public function getPatch(
1015
        ?string $name = null,
1016
        ?int $filter = null,
1017
        array | int $filterOptions = 0
1018
    ) : mixed {
1019
        if ($this->isMethod(Method::PATCH)) {
1✔
1020
            return $this->getFilteredParsedBody($name, $filter, $filterOptions);
1✔
1021
        }
1022
        return $name === null ? [] : null;
1✔
1023
    }
1024

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

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

1070
    /**
1071
     * Get $_SERVER variables.
1072
     *
1073
     * @param string|null $name
1074
     * @param int|null $filter
1075
     * @param array<int,int>|int $filterOptions
1076
     *
1077
     * @return mixed
1078
     */
1079
    public function getServer(
1080
        ?string $name = null,
1081
        ?int $filter = null,
1082
        array | int $filterOptions = 0
1083
    ) : mixed {
1084
        return $this->filterInput(\INPUT_SERVER, $name, $filter, $filterOptions);
12✔
1085
    }
1086

1087
    /**
1088
     * Gets the requested URL.
1089
     *
1090
     * @return URL
1091
     */
1092
    #[Override]
1093
    #[Pure]
1094
    public function getUrl() : URL
1095
    {
1096
        return parent::getUrl();
162✔
1097
    }
1098

1099
    /**
1100
     * Gets the User Agent client.
1101
     *
1102
     * @return UserAgent|null the UserAgent object or null if no user-agent
1103
     * header was received
1104
     */
1105
    public function getUserAgent() : ?UserAgent
1106
    {
1107
        if (isset($this->userAgent) && $this->userAgent instanceof UserAgent) {
13✔
1108
            return $this->userAgent;
1✔
1109
        }
1110
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
13✔
1111
        $userAgent ? $this->setUserAgent($userAgent) : $this->userAgent = false;
13✔
1112
        return $this->userAgent ?: null;
13✔
1113
    }
1114

1115
    /**
1116
     * @param UserAgent|string $userAgent
1117
     *
1118
     * @return static
1119
     */
1120
    protected function setUserAgent(UserAgent | string $userAgent) : static
1121
    {
1122
        if (!$userAgent instanceof UserAgent) {
4✔
1123
            $userAgent = new UserAgent($userAgent);
4✔
1124
        }
1125
        $this->userAgent = $userAgent;
4✔
1126
        return $this;
4✔
1127
    }
1128

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

1147
    /**
1148
     * Say if a connection has HTTPS.
1149
     *
1150
     * @return bool
1151
     */
1152
    public function isSecure() : bool
1153
    {
1154
        if (isset($this->isSecure)) {
162✔
1155
            return $this->isSecure;
16✔
1156
        }
1157
        $scheme = $_SERVER['REQUEST_SCHEME'] ?? null;
162✔
1158
        $https = $_SERVER['HTTPS'] ?? null;
162✔
1159
        return $this->isSecure = ($scheme === 'https' || $https === 'on');
162✔
1160
    }
1161

1162
    /**
1163
     * Tells if the Content-Type header is application/x-www-form-urlencoded.
1164
     *
1165
     * @return bool
1166
     */
1167
    public function isFormUrlEncoded() : bool
1168
    {
1169
        return $this->parseContentType() === 'application/x-www-form-urlencoded';
17✔
1170
    }
1171

1172
    /**
1173
     * Tells if the Content-Type header is multipart/form-data.
1174
     *
1175
     * @return bool
1176
     */
1177
    public function isFormData() : bool
1178
    {
1179
        return $this->parseContentType() === 'multipart/form-data';
5✔
1180
    }
1181

1182
    /**
1183
     * Tells if the Content-Type header is application/x-www-form-urlencoded or
1184
     * multipart/form-data.
1185
     *
1186
     * @return bool
1187
     */
1188
    public function isForm() : bool
1189
    {
1190
        return $this->isFormUrlEncoded() || $this->isFormData();
6✔
1191
    }
1192

1193
    /**
1194
     * Say if the request is a JSON call.
1195
     *
1196
     * @return bool
1197
     */
1198
    public function isJson() : bool
1199
    {
1200
        return $this->parseContentType() === 'application/json';
1✔
1201
    }
1202

1203
    /**
1204
     * Say if the request method is POST.
1205
     *
1206
     * @return bool
1207
     */
1208
    public function isPost() : bool
1209
    {
1210
        return $this->isMethod(Method::POST);
13✔
1211
    }
1212

1213
    /**
1214
     * @see https://php.watch/codex/enable_post_data_reading
1215
     *
1216
     * @return bool
1217
     */
1218
    protected function isEnabledPostDataReading() : bool
1219
    {
1220
        return \ini_get('enable_post_data_reading') === '1';
23✔
1221
    }
1222

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

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

1276
    /**
1277
     * Make a Response with the current Request.
1278
     *
1279
     * @return Response
1280
     */
1281
    public function makeResponse() : Response
1282
    {
1283
        return new Response($this);
1✔
1284
    }
1285
}
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