Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
286 / 286 |
|
100.00% |
62 / 62 |
CRAP | |
100.00% |
1 / 1 |
Request | |
100.00% |
286 / 286 |
|
100.00% |
62 / 62 |
138 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
__call | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
__toString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getMultipartBody | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
6 | |||
validateHost | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
prepareStatusLine | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getHeader | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHeaders | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
prepareHeaders | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getCookie | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCookies | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
prepareCookies | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getBody | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
prepareFiles | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
filterInput | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
forceHttps | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getAuthType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getBasicAuth | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getBearerAuth | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getDigestAuth | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
parseAuth | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
parseBasicAuth | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
parseBearerAuth | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
parseDigestAuth | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
4 | |||
getParsedBody | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
getJson | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getNegotiableValues | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
negotiate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getAccepts | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
negotiateAccept | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCharsets | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
negotiateCharset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEncodings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
negotiateEncoding | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
negotiateLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEnv | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFiles | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
hasFiles | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getGet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHost | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getIp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRedirectData | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 | |||
getPort | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPost | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProxiedIp | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
getReferer | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
getServer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserAgent | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
setUserAgent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isAjax | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isSecure | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isForm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isJson | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPost | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getInputFiles | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
setHost | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 |
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\Deprecated; |
17 | use JetBrains\PhpStorm\Pure; |
18 | use LogicException; |
19 | use stdClass; |
20 | use UnexpectedValueException; |
21 | |
22 | /** |
23 | * Class Request. |
24 | * |
25 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#HTTP_Requests |
26 | * |
27 | * @package http |
28 | */ |
29 | class Request extends Message implements RequestInterface |
30 | { |
31 | /** |
32 | * @var array<string,array<mixed>|UploadedFile> |
33 | */ |
34 | protected array $files = []; |
35 | /** |
36 | * @var array<string,mixed>|null |
37 | */ |
38 | protected ?array $parsedBody = null; |
39 | /** |
40 | * HTTP Authorization Header parsed. |
41 | * |
42 | * @var array<string,string|null>|null |
43 | */ |
44 | protected ?array $auth = null; |
45 | /** |
46 | * @var string|null Basic or Digest |
47 | */ |
48 | protected ?string $authType = null; |
49 | protected string $host; |
50 | protected int $port; |
51 | /** |
52 | * Request X-Request-ID header. |
53 | */ |
54 | protected string | false $id; |
55 | /** |
56 | * @var array<string,array<mixed>|null> |
57 | */ |
58 | protected array $negotiation = [ |
59 | 'ACCEPT' => null, |
60 | 'CHARSET' => null, |
61 | 'ENCODING' => null, |
62 | 'LANGUAGE' => null, |
63 | ]; |
64 | protected false | URL $referrer; |
65 | protected false | UserAgent $userAgent; |
66 | protected bool $isAjax; |
67 | /** |
68 | * Tell if is a HTTPS connection. |
69 | * |
70 | * @var bool |
71 | */ |
72 | protected bool $isSecure; |
73 | |
74 | /** |
75 | * Request constructor. |
76 | * |
77 | * @param array<string> $allowedHosts set allowed hosts if your |
78 | * server don't serve by Host header, as Nginx do |
79 | * |
80 | * @throws UnexpectedValueException if invalid Host |
81 | */ |
82 | public function __construct(array $allowedHosts = []) |
83 | { |
84 | if ($allowedHosts) { |
85 | $this->validateHost($allowedHosts); |
86 | } |
87 | $this->prepareStatusLine(); |
88 | } |
89 | |
90 | /** |
91 | * @param string $method |
92 | * @param array<int,mixed> $arguments |
93 | * |
94 | * @throws BadMethodCallException for method not allowed or method not found |
95 | * |
96 | * @return static |
97 | */ |
98 | public function __call(string $method, array $arguments) |
99 | { |
100 | if ($method === 'setBody') { |
101 | return $this->setBody(...$arguments); |
102 | } |
103 | if (\method_exists($this, $method)) { |
104 | throw new BadMethodCallException("Method not allowed: {$method}"); |
105 | } |
106 | throw new BadMethodCallException("Method not found: {$method}"); |
107 | } |
108 | |
109 | public function __toString() : string |
110 | { |
111 | if ($this->parseContentType() === 'multipart/form-data') { |
112 | $this->setBody($this->getMultipartBody()); |
113 | } |
114 | return parent::__toString(); |
115 | } |
116 | |
117 | /** |
118 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data |
119 | * |
120 | * @return string |
121 | */ |
122 | protected function getMultipartBody() : string |
123 | { |
124 | $bodyParts = []; |
125 | /** |
126 | * @var array<string,string> $post |
127 | */ |
128 | $post = ArraySimple::convert($this->getPost()); |
129 | foreach ($post as $field => $value) { |
130 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); |
131 | $bodyParts[] = \implode("\r\n", [ |
132 | "Content-Disposition: form-data; name=\"{$field}\"", |
133 | '', |
134 | $value, |
135 | ]); |
136 | } |
137 | /** |
138 | * @var array<string,UploadedFile> $files |
139 | */ |
140 | $files = ArraySimple::convert($this->getFiles()); |
141 | foreach ($files as $field => $file) { |
142 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); |
143 | $filename = \htmlspecialchars($file->getName(), \ENT_QUOTES | \ENT_HTML5); |
144 | $getContentsOf = $file->isMoved() ? $file->getDestination() : $file->getTmpName(); |
145 | $data = ''; |
146 | if ($getContentsOf !== '') { |
147 | $data = \file_get_contents($getContentsOf); |
148 | } |
149 | $bodyParts[] = \implode("\r\n", [ |
150 | "Content-Disposition: form-data; name=\"{$field}\"; filename=\"{$filename}\"", |
151 | 'Content-Type: ' . $file->getClientType(), |
152 | '', |
153 | $data, |
154 | ]); |
155 | } |
156 | $boundary = \explode(';', $this->getContentType(), 2); |
157 | $boundary = \trim($boundary[1]); |
158 | $boundary = \substr($boundary, \strlen('boundary=')); |
159 | foreach ($bodyParts as &$part) { |
160 | $part = "--{$boundary}\r\n{$part}"; |
161 | } |
162 | unset($part); |
163 | $bodyParts[] = "--{$boundary}--"; |
164 | $bodyParts[] = ''; |
165 | $bodyParts = \implode("\r\n", $bodyParts); |
166 | /** |
167 | * Uncomment the code below to make a raw test. |
168 | * |
169 | * @see \Tests\HTTP\RequestTest::testToStringMultipart() |
170 | */ |
171 | /* |
172 | $serverLength = (string) $_SERVER['CONTENT_LENGTH']; |
173 | $algoLength = (string) \strlen($bodyParts); |
174 | if ($serverLength !== $algoLength) { |
175 | throw new \Exception( |
176 | '$_SERVER CONTENT_LENGTH is ' . $serverLength |
177 | . ', but the algorithm calculated ' . $algoLength |
178 | ); |
179 | } |
180 | */ |
181 | return $bodyParts; |
182 | } |
183 | |
184 | /** |
185 | * Check if Host header is allowed. |
186 | * |
187 | * @see https://expressionengine.com/blog/http-host-and-server-name-security-issues |
188 | * @see http://nginx.org/en/docs/http/request_processing.html |
189 | * |
190 | * @param array<string> $allowedHosts |
191 | */ |
192 | protected function validateHost(array $allowedHosts) : void |
193 | { |
194 | $host = $_SERVER['HTTP_HOST'] ?? null; |
195 | if ( ! \in_array($host, $allowedHosts, true)) { |
196 | throw new UnexpectedValueException('Invalid Host: ' . $host); |
197 | } |
198 | } |
199 | |
200 | protected function prepareStatusLine() : void |
201 | { |
202 | $this->setProtocol($_SERVER['SERVER_PROTOCOL']); |
203 | $this->setMethod($_SERVER['REQUEST_METHOD']); |
204 | $url = $this->isSecure() ? 'https' : 'http'; |
205 | $url .= '://' . $_SERVER['HTTP_HOST']; |
206 | $url .= $_SERVER['REQUEST_URI']; |
207 | $this->setUrl($url); |
208 | $this->setHost($this->getUrl()->getHost()); |
209 | } |
210 | |
211 | public function getHeader(string $name) : ?string |
212 | { |
213 | $this->prepareHeaders(); |
214 | return $this->headers[\strtolower($name)] ?? null; |
215 | } |
216 | |
217 | /** |
218 | * @return array<string,string> |
219 | */ |
220 | public function getHeaders() : array |
221 | { |
222 | $this->prepareHeaders(); |
223 | return $this->headers; |
224 | } |
225 | |
226 | protected function prepareHeaders() : void |
227 | { |
228 | if ( ! empty($this->headers)) { |
229 | return; |
230 | } |
231 | foreach ($_SERVER as $name => $value) { |
232 | if (\str_starts_with($name, 'HTTP_')) { |
233 | $name = \strtr(\substr($name, 5), ['_' => '-']); |
234 | $this->setHeader($name, $value); |
235 | } |
236 | } |
237 | } |
238 | |
239 | public function getCookie(string $name) : ?Cookie |
240 | { |
241 | $this->prepareCookies(); |
242 | return $this->cookies[$name] ?? null; |
243 | } |
244 | |
245 | /** |
246 | * Get all Cookies. |
247 | * |
248 | * @return array<string,Cookie> |
249 | */ |
250 | public function getCookies() : array |
251 | { |
252 | $this->prepareCookies(); |
253 | return $this->cookies; |
254 | } |
255 | |
256 | protected function prepareCookies() : void |
257 | { |
258 | if ( ! empty($this->cookies)) { |
259 | return; |
260 | } |
261 | foreach ($_COOKIE as $name => $value) { |
262 | $this->setCookie(new Cookie($name, $value)); |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * @see https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input |
268 | * |
269 | * @return string |
270 | */ |
271 | public function getBody() : string |
272 | { |
273 | if ( ! isset($this->body)) { |
274 | $this->body = (string) \file_get_contents('php://input'); |
275 | } |
276 | return $this->body; |
277 | } |
278 | |
279 | protected function prepareFiles() : void |
280 | { |
281 | if ( ! empty($this->files)) { |
282 | return; |
283 | } |
284 | $this->files = $this->getInputFiles(); |
285 | } |
286 | |
287 | /** |
288 | * @param int $type |
289 | * @param string|null $name |
290 | * @param int|null $filter |
291 | * @param array<int,int>|int $options |
292 | * |
293 | * @see https://www.php.net/manual/en/function.filter-var |
294 | * @see https://www.php.net/manual/en/filter.filters.php |
295 | * |
296 | * @return mixed |
297 | */ |
298 | protected function filterInput( |
299 | int $type, |
300 | string $name = null, |
301 | int $filter = null, |
302 | array | int $options = 0 |
303 | ) : mixed { |
304 | $input = match ($type) { |
305 | \INPUT_POST => $_POST, |
306 | \INPUT_GET => $_GET, |
307 | \INPUT_COOKIE => $_COOKIE, |
308 | \INPUT_ENV => $_ENV, |
309 | \INPUT_SERVER => $_SERVER, |
310 | default => throw new InvalidArgumentException('Invalid input type: ' . $type) |
311 | }; |
312 | if ($name !== null) { |
313 | $input = \in_array($type, [\INPUT_POST, \INPUT_GET], true) |
314 | ? ArraySimple::value($name, $input) |
315 | : $input[$name] ?? null; |
316 | } |
317 | if ($filter !== null) { |
318 | $input = \filter_var($input, $filter, $options); |
319 | } |
320 | return $input; |
321 | } |
322 | |
323 | /** |
324 | * Force an HTTPS connection on same URL. |
325 | */ |
326 | public function forceHttps() : void |
327 | { |
328 | if ( ! $this->isSecure()) { |
329 | \header( |
330 | 'Location: ' . $this->getUrl()->setScheme('https'), |
331 | true, |
332 | Status::MOVED_PERMANENTLY |
333 | ); |
334 | if ( ! \defined('TESTING')) { |
335 | // @codeCoverageIgnoreStart |
336 | exit; |
337 | // @codeCoverageIgnoreEnd |
338 | } |
339 | } |
340 | } |
341 | |
342 | /** |
343 | * Get the Authorization type. |
344 | * |
345 | * @return string|null Basic, Bearer, Digest or null for none |
346 | */ |
347 | public function getAuthType() : ?string |
348 | { |
349 | if ($this->authType === null) { |
350 | $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? null; |
351 | if ($auth) { |
352 | $this->parseAuth($auth); |
353 | } |
354 | } |
355 | return $this->authType; |
356 | } |
357 | |
358 | /** |
359 | * Get Basic authorization. |
360 | * |
361 | * @return array<string>|null Two keys: username and password |
362 | */ |
363 | #[ArrayShape(['username' => 'string|null', 'password' => 'string|null'])] |
364 | public function getBasicAuth() : ?array |
365 | { |
366 | return $this->getAuthType() === 'Basic' |
367 | ? $this->auth |
368 | : null; |
369 | } |
370 | |
371 | /** |
372 | * Get Bearer authorization. |
373 | * |
374 | * @return array<string>|null One key: token |
375 | */ |
376 | #[ArrayShape(['token' => 'string|null'])] |
377 | public function getBearerAuth() : ?array |
378 | { |
379 | return $this->getAuthType() === 'Bearer' |
380 | ? $this->auth |
381 | : null; |
382 | } |
383 | |
384 | /** |
385 | * Get Digest authorization. |
386 | * |
387 | * @return array<string>|null Nine keys: username, realm, nonce, uri, |
388 | * response, opaque, qop, nc, cnonce |
389 | */ |
390 | #[ArrayShape([ |
391 | 'username' => 'string|null', |
392 | 'realm' => 'string|null', |
393 | 'nonce' => 'string|null', |
394 | 'uri' => 'string|null', |
395 | 'response' => 'string|null', |
396 | 'opaque' => 'string|null', |
397 | 'qop' => 'string|null', |
398 | 'nc' => 'string|null', |
399 | 'cnonce' => 'string|null', |
400 | ])] |
401 | public function getDigestAuth() : ?array |
402 | { |
403 | return $this->getAuthType() === 'Digest' |
404 | ? $this->auth |
405 | : null; |
406 | } |
407 | |
408 | /** |
409 | * @param string $authorization |
410 | * |
411 | * @return array<string,string|null> |
412 | */ |
413 | protected function parseAuth(string $authorization) : array |
414 | { |
415 | $this->auth = []; |
416 | [$type, $attributes] = \array_pad(\explode(' ', $authorization, 2), 2, null); |
417 | if ($type === 'Basic') { |
418 | $this->authType = $type; |
419 | $this->auth = $this->parseBasicAuth($attributes); |
420 | } elseif ($type === 'Bearer') { |
421 | $this->authType = $type; |
422 | $this->auth = $this->parseBearerAuth($attributes); |
423 | } elseif ($type === 'Digest') { |
424 | $this->authType = $type; |
425 | $this->auth = $this->parseDigestAuth($attributes); |
426 | } |
427 | return $this->auth; |
428 | } |
429 | |
430 | /** |
431 | * @param string $attributes |
432 | * |
433 | * @return array<string,string|null> |
434 | */ |
435 | #[ArrayShape(['username' => 'string|null', 'password' => 'string|null'])] |
436 | #[Pure] |
437 | protected function parseBasicAuth(string $attributes) : array |
438 | { |
439 | $data = [ |
440 | 'username' => null, |
441 | 'password' => null, |
442 | ]; |
443 | $attributes = \base64_decode($attributes); |
444 | if ($attributes) { |
445 | [ |
446 | $data['username'], |
447 | $data['password'], |
448 | ] = \array_pad(\explode(':', $attributes, 2), 2, null); |
449 | } |
450 | return $data; |
451 | } |
452 | |
453 | /** |
454 | * @param string $attributes |
455 | * |
456 | * @return array<string,string|null> |
457 | */ |
458 | #[ArrayShape(['token' => 'string|null'])] |
459 | #[Pure] |
460 | protected function parseBearerAuth(string $attributes) : array |
461 | { |
462 | $data = [ |
463 | 'token' => null, |
464 | ]; |
465 | if ($attributes) { |
466 | $data['token'] = $attributes; |
467 | } |
468 | return $data; |
469 | } |
470 | |
471 | /** |
472 | * @param string $attributes |
473 | * |
474 | * @return array<string,string|null> |
475 | */ |
476 | #[ArrayShape([ |
477 | 'username' => 'string|null', |
478 | 'realm' => 'string|null', |
479 | 'nonce' => 'string|null', |
480 | 'uri' => 'string|null', |
481 | 'response' => 'string|null', |
482 | 'opaque' => 'string|null', |
483 | 'qop' => 'string|null', |
484 | 'nc' => 'string|null', |
485 | 'cnonce' => 'string|null', |
486 | ])] |
487 | protected function parseDigestAuth(string $attributes) : array |
488 | { |
489 | $data = [ |
490 | 'username' => null, |
491 | 'realm' => null, |
492 | 'nonce' => null, |
493 | 'uri' => null, |
494 | 'response' => null, |
495 | 'opaque' => null, |
496 | 'qop' => null, |
497 | 'nc' => null, |
498 | 'cnonce' => null, |
499 | ]; |
500 | \preg_match_all( |
501 | '#(username|realm|nonce|uri|response|opaque|qop|nc|cnonce)=(?:([\'"])([^\2]+?)\2|([^\s,]+))#', |
502 | $attributes, |
503 | $matches, |
504 | \PREG_SET_ORDER |
505 | ); |
506 | foreach ($matches as $match) { |
507 | if (isset($match[1], $match[3])) { |
508 | $data[$match[1]] = $match[3] ?: $match[4] ?? ''; |
509 | } |
510 | } |
511 | return $data; |
512 | } |
513 | |
514 | /** |
515 | * Get the Parsed Body or part of it. |
516 | * |
517 | * @param string|null $name |
518 | * @param int|null $filter |
519 | * @param array<int,int>|int $filterOptions |
520 | * |
521 | * @see Request::filterInput() |
522 | * |
523 | * @return array<int|string,mixed>|mixed|string|null |
524 | */ |
525 | public function getParsedBody( |
526 | string $name = null, |
527 | int $filter = null, |
528 | array | int $filterOptions = 0 |
529 | ) { |
530 | if ($this->getMethod() === Method::POST) { |
531 | return $this->getPost($name, $filter, $filterOptions); |
532 | } |
533 | if ($this->parsedBody === null) { |
534 | $this->isForm() |
535 | ? \parse_str($this->getBody(), $this->parsedBody) |
536 | : $this->parsedBody = []; |
537 | } |
538 | $variable = $name === null |
539 | ? $this->parsedBody |
540 | : ArraySimple::value($name, $this->parsedBody); |
541 | return $filter !== null |
542 | ? \filter_var($variable, $filter, $filterOptions) |
543 | : $variable; |
544 | } |
545 | |
546 | /** |
547 | * Get the request body as JSON. |
548 | * |
549 | * @param bool|null $associative When true, JSON objects will be returned as |
550 | * associative arrays; when false, JSON objects will be returned as objects. |
551 | * When null, JSON objects will be returned as associative arrays or objects |
552 | * depending on whether JSON_OBJECT_AS_ARRAY is set in the flags. |
553 | * @param int $flags <p> |
554 | * Bitmask of JSON decode options:<br/> |
555 | * {@see \JSON_BIGINT_AS_STRING} decodes large integers as their original |
556 | * string value.<br/> |
557 | * {@see \JSON_INVALID_UTF8_IGNORE} ignores invalid UTF-8 characters,<br/> |
558 | * {@see \JSON_INVALID_UTF8_SUBSTITUTE} converts invalid UTF-8 characters to |
559 | * \0xfffd,<br/> |
560 | * {@see \JSON_OBJECT_AS_ARRAY} decodes JSON objects as PHP array, since |
561 | * 7.2.0 used by default if $assoc parameter is null,<br/> |
562 | * {@see \JSON_THROW_ON_ERROR} when passed this flag, the error behaviour of |
563 | * these functions is changed. The global error state is left untouched, and |
564 | * if an error occurs that would otherwise set it, these functions instead |
565 | * throw a JsonException<br/> |
566 | * </p> |
567 | * @param int<1,max> $depth user specified recursion depth |
568 | * |
569 | * @see https://www.php.net/manual/en/function.json-decode.php |
570 | * @see https://www.php.net/manual/en/json.constants.php |
571 | * |
572 | * @return array<string,mixed>|false|stdClass If option JSON_THROW_ON_ERROR |
573 | * is not set, return false if json_decode fail. Otherwise return a |
574 | * stdClass instance, or an array if the $associative argument is passed as |
575 | * true. |
576 | */ |
577 | public function getJson( |
578 | ?bool $associative = null, |
579 | int $flags = 0, |
580 | int $depth = 512 |
581 | ) : array | stdClass | false { |
582 | $body = \json_decode($this->getBody(), $associative, $depth, $flags); |
583 | if (\json_last_error() !== \JSON_ERROR_NONE) { |
584 | return false; |
585 | } |
586 | return $body; |
587 | } |
588 | |
589 | /** |
590 | * @param string $type |
591 | * |
592 | * @return array<int,string> |
593 | */ |
594 | protected function getNegotiableValues(string $type) : array |
595 | { |
596 | if ($this->negotiation[$type]) { |
597 | return $this->negotiation[$type]; |
598 | } |
599 | $header = $_SERVER['HTTP_ACCEPT' . ($type !== 'ACCEPT' ? '_' . $type : '')] ?? null; |
600 | $this->negotiation[$type] = \array_keys(static::parseQualityValues( |
601 | $header |
602 | )); |
603 | $this->negotiation[$type] = \array_map('\strtolower', $this->negotiation[$type]); |
604 | return $this->negotiation[$type]; |
605 | } |
606 | |
607 | /** |
608 | * @param string $type |
609 | * @param array<int,string> $negotiable |
610 | * |
611 | * @return string |
612 | */ |
613 | protected function negotiate(string $type, array $negotiable) : string |
614 | { |
615 | $negotiable = \array_map('\strtolower', $negotiable); |
616 | foreach ($this->getNegotiableValues($type) as $item) { |
617 | if (\in_array($item, $negotiable, true)) { |
618 | return $item; |
619 | } |
620 | } |
621 | return $negotiable[0]; |
622 | } |
623 | |
624 | /** |
625 | * Get the mime types of the Accept header. |
626 | * |
627 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept |
628 | * |
629 | * @return array<int,string> |
630 | */ |
631 | public function getAccepts() : array |
632 | { |
633 | return $this->getNegotiableValues('ACCEPT'); |
634 | } |
635 | |
636 | /** |
637 | * Negotiate the Accept header. |
638 | * |
639 | * @param array<int,string> $negotiable Allowed mime types |
640 | * |
641 | * @return string The negotiated mime type |
642 | */ |
643 | public function negotiateAccept(array $negotiable) : string |
644 | { |
645 | return $this->negotiate('ACCEPT', $negotiable); |
646 | } |
647 | |
648 | /** |
649 | * Get the Accept-Charset's. |
650 | * |
651 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset |
652 | * |
653 | * @return array<int,string> |
654 | */ |
655 | public function getCharsets() : array |
656 | { |
657 | return $this->getNegotiableValues('CHARSET'); |
658 | } |
659 | |
660 | /** |
661 | * Negotiate the Accept-Charset. |
662 | * |
663 | * @param array<int,string> $negotiable Allowed charsets |
664 | * |
665 | * @return string The negotiated charset |
666 | */ |
667 | public function negotiateCharset(array $negotiable) : string |
668 | { |
669 | return $this->negotiate('CHARSET', $negotiable); |
670 | } |
671 | |
672 | /** |
673 | * Get the Accept-Encoding. |
674 | * |
675 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding |
676 | * |
677 | * @return array<int,string> |
678 | */ |
679 | public function getEncodings() : array |
680 | { |
681 | return $this->getNegotiableValues('ENCODING'); |
682 | } |
683 | |
684 | /** |
685 | * Negotiate the Accept-Encoding. |
686 | * |
687 | * @param array<int,string> $negotiable The allowed encodings |
688 | * |
689 | * @return string The negotiated encoding |
690 | */ |
691 | public function negotiateEncoding(array $negotiable) : string |
692 | { |
693 | return $this->negotiate('ENCODING', $negotiable); |
694 | } |
695 | |
696 | /** |
697 | * Get the Accept-Language's. |
698 | * |
699 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language |
700 | * |
701 | * @return array<int,string> |
702 | */ |
703 | public function getLanguages() : array |
704 | { |
705 | return $this->getNegotiableValues('LANGUAGE'); |
706 | } |
707 | |
708 | /** |
709 | * Negotiated the Accept-Language. |
710 | * |
711 | * @param array<int,string> $negotiable Allowed languages |
712 | * |
713 | * @return string The negotiated language |
714 | */ |
715 | public function negotiateLanguage(array $negotiable) : string |
716 | { |
717 | return $this->negotiate('LANGUAGE', $negotiable); |
718 | } |
719 | |
720 | /** |
721 | * Get the Content-Type header value. |
722 | * |
723 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type |
724 | * |
725 | * @return string|null |
726 | */ |
727 | #[Pure] |
728 | public function getContentType() : ?string |
729 | { |
730 | return $_SERVER['HTTP_CONTENT_TYPE'] ?? null; |
731 | } |
732 | |
733 | /** |
734 | * @param string|null $name |
735 | * @param int|null $filter |
736 | * @param array<int,int>|int $filterOptions |
737 | * |
738 | * @see Request::filterInput() |
739 | * |
740 | * @return mixed |
741 | */ |
742 | public function getEnv( |
743 | string $name = null, |
744 | int $filter = null, |
745 | array | int $filterOptions = 0 |
746 | ) : mixed { |
747 | return $this->filterInput(\INPUT_ENV, $name, $filter, $filterOptions); |
748 | } |
749 | |
750 | /** |
751 | * @return array<string,array<mixed>|UploadedFile> |
752 | */ |
753 | public function getFiles() : array |
754 | { |
755 | $this->prepareFiles(); |
756 | return $this->files; |
757 | } |
758 | |
759 | public function hasFiles() : bool |
760 | { |
761 | $this->prepareFiles(); |
762 | return ! empty($this->files); |
763 | } |
764 | |
765 | public function getFile(string $name) : ?UploadedFile |
766 | { |
767 | $this->prepareFiles(); |
768 | $file = ArraySimple::value($name, $this->files); |
769 | return \is_array($file) ? null : $file; |
770 | } |
771 | |
772 | /** |
773 | * Get the URL GET queries. |
774 | * |
775 | * @param string|null $name |
776 | * @param int|null $filter |
777 | * @param array<int,int>|int $filterOptions |
778 | * |
779 | * @see Request::filterInput() |
780 | * |
781 | * @return mixed |
782 | */ |
783 | public function getGet( |
784 | string $name = null, |
785 | int $filter = null, |
786 | array | int $filterOptions = 0 |
787 | ) : mixed { |
788 | return $this->filterInput(\INPUT_GET, $name, $filter, $filterOptions); |
789 | } |
790 | |
791 | /** |
792 | * @return string |
793 | */ |
794 | #[Pure] |
795 | public function getHost() : string |
796 | { |
797 | return $this->host; |
798 | } |
799 | |
800 | /** |
801 | * Get the X-Request-ID header. |
802 | * |
803 | * @return string|null |
804 | */ |
805 | public function getId() : string | null |
806 | { |
807 | if (isset($this->id)) { |
808 | return $this->id === false ? null : $this->id; |
809 | } |
810 | $this->id = $_SERVER['HTTP_X_REQUEST_ID'] ?? false; |
811 | return $this->getId(); |
812 | } |
813 | |
814 | /** |
815 | * Get the connection IP. |
816 | * |
817 | * @return string |
818 | */ |
819 | public function getIp() : string |
820 | { |
821 | return $_SERVER['REMOTE_ADDR']; |
822 | } |
823 | |
824 | #[Pure] |
825 | public function getMethod() : string |
826 | { |
827 | return parent::getMethod(); |
828 | } |
829 | |
830 | /** |
831 | * @param string $method |
832 | * |
833 | * @throws InvalidArgumentException for invalid method |
834 | * |
835 | * @return bool |
836 | */ |
837 | public function isMethod(string $method) : bool |
838 | { |
839 | return parent::isMethod($method); |
840 | } |
841 | |
842 | /** |
843 | * Gets data from the last request, if it was redirected. |
844 | * |
845 | * @param string|null $key a key name or null to get all data |
846 | * |
847 | * @see Response::redirect() |
848 | * |
849 | * @throws LogicException if PHP Session is not active to get redirect data |
850 | * |
851 | * @return mixed an array containing all data, the key value or null |
852 | * if the key was not found |
853 | */ |
854 | public function getRedirectData(string $key = null) : mixed |
855 | { |
856 | static $data; |
857 | if ($data === null && \session_status() !== \PHP_SESSION_ACTIVE) { |
858 | throw new LogicException('Session must be active to get redirect data'); |
859 | } |
860 | if ($data === null) { |
861 | $data = $_SESSION['$']['redirect_data'] ?? false; |
862 | unset($_SESSION['$']['redirect_data']); |
863 | } |
864 | if ($key !== null && $data) { |
865 | return ArraySimple::value($key, $data); |
866 | } |
867 | return $data === false ? null : $data; |
868 | } |
869 | |
870 | /** |
871 | * Get the URL port. |
872 | * |
873 | * @return int |
874 | */ |
875 | public function getPort() : int |
876 | { |
877 | return $this->port ?? $_SERVER['SERVER_PORT']; |
878 | } |
879 | |
880 | /** |
881 | * Get POST data. |
882 | * |
883 | * @param string|null $name |
884 | * @param int|null $filter |
885 | * @param array<int,int>|int $filterOptions |
886 | * |
887 | * @see Request::filterInput() |
888 | * |
889 | * @return mixed |
890 | */ |
891 | public function getPost( |
892 | string $name = null, |
893 | int $filter = null, |
894 | array | int $filterOptions = 0 |
895 | ) : mixed { |
896 | return $this->filterInput(\INPUT_POST, $name, $filter, $filterOptions); |
897 | } |
898 | |
899 | /** |
900 | * Get the connection IP via a proxy header. |
901 | * |
902 | * @return string|null |
903 | * |
904 | * @deprecated Use {@see Request::getHeader()} |
905 | * |
906 | * @codeCoverageIgnore |
907 | */ |
908 | #[Deprecated( |
909 | reason: 'since HTTP Library version 5.5, use getHeader() instead', |
910 | replacement: '%class%->getHeader()' |
911 | )] |
912 | public function getProxiedIp() : ?string |
913 | { |
914 | \trigger_error( |
915 | 'Method ' . __METHOD__ . ' is deprecated', |
916 | \E_USER_DEPRECATED |
917 | ); |
918 | foreach ([ |
919 | 'X-Real-IP', |
920 | 'X-Forwarded-For', |
921 | 'Client-IP', |
922 | 'X-Client-IP', |
923 | 'X-Cluster-Client-IP', |
924 | ] as $header) { |
925 | $header = $this->getHeader($header); |
926 | if ($header) { |
927 | $ip = \explode(',', $header, 2)[0]; |
928 | return \trim($ip); |
929 | } |
930 | } |
931 | return null; |
932 | } |
933 | |
934 | /** |
935 | * Get the Referer header. |
936 | * |
937 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer |
938 | * |
939 | * @return URL|null |
940 | */ |
941 | public function getReferer() : ?URL |
942 | { |
943 | if ( ! isset($this->referrer)) { |
944 | $this->referrer = false; |
945 | $referer = $_SERVER['HTTP_REFERER'] ?? null; |
946 | if ($referer !== null) { |
947 | try { |
948 | $this->referrer = new URL($referer); |
949 | } catch (InvalidArgumentException) { |
950 | $this->referrer = false; |
951 | } |
952 | } |
953 | } |
954 | return $this->referrer ?: null; |
955 | } |
956 | |
957 | /** |
958 | * Get $_SERVER variables. |
959 | * |
960 | * @param string|null $name |
961 | * @param int|null $filter |
962 | * @param array<int,int>|int $filterOptions |
963 | * |
964 | * @see Request::filterInput() |
965 | * |
966 | * @return mixed |
967 | */ |
968 | public function getServer( |
969 | string $name = null, |
970 | int $filter = null, |
971 | array | int $filterOptions = 0 |
972 | ) : mixed { |
973 | return $this->filterInput(\INPUT_SERVER, $name, $filter, $filterOptions); |
974 | } |
975 | |
976 | /** |
977 | * Gets the requested URL. |
978 | * |
979 | * @return URL |
980 | */ |
981 | #[Pure] |
982 | public function getUrl() : URL |
983 | { |
984 | return parent::getUrl(); |
985 | } |
986 | |
987 | /** |
988 | * Gets the User Agent client. |
989 | * |
990 | * @return UserAgent|null the UserAgent object or null if no user-agent |
991 | * header was received |
992 | */ |
993 | public function getUserAgent() : ?UserAgent |
994 | { |
995 | if (isset($this->userAgent) && $this->userAgent instanceof UserAgent) { |
996 | return $this->userAgent; |
997 | } |
998 | $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null; |
999 | $userAgent ? $this->setUserAgent($userAgent) : $this->userAgent = false; |
1000 | return $this->userAgent ?: null; |
1001 | } |
1002 | |
1003 | /** |
1004 | * @param string|UserAgent $userAgent |
1005 | * |
1006 | * @return static |
1007 | */ |
1008 | protected function setUserAgent(string | UserAgent $userAgent) : static |
1009 | { |
1010 | if ( ! $userAgent instanceof UserAgent) { |
1011 | $userAgent = new UserAgent($userAgent); |
1012 | } |
1013 | $this->userAgent = $userAgent; |
1014 | return $this; |
1015 | } |
1016 | |
1017 | /** |
1018 | * Check if is an AJAX Request based in the X-Requested-With Header. |
1019 | * |
1020 | * The X-Requested-With Header containing the "XMLHttpRequest" value is |
1021 | * used by various javascript libraries. |
1022 | * |
1023 | * @return bool |
1024 | */ |
1025 | public function isAjax() : bool |
1026 | { |
1027 | if (isset($this->isAjax)) { |
1028 | return $this->isAjax; |
1029 | } |
1030 | $received = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? null; |
1031 | return $this->isAjax = ($received |
1032 | && \strtolower($received) === 'xmlhttprequest'); |
1033 | } |
1034 | |
1035 | /** |
1036 | * Say if a connection has HTTPS. |
1037 | * |
1038 | * @return bool |
1039 | */ |
1040 | public function isSecure() : bool |
1041 | { |
1042 | if (isset($this->isSecure)) { |
1043 | return $this->isSecure; |
1044 | } |
1045 | $scheme = $_SERVER['REQUEST_SCHEME'] ?? null; |
1046 | $https = $_SERVER['HTTPS'] ?? null; |
1047 | return $this->isSecure = ($scheme === 'https' || $https === 'on'); |
1048 | } |
1049 | |
1050 | /** |
1051 | * Say if the request is done with application/x-www-form-urlencoded |
1052 | * Content-Type. |
1053 | * |
1054 | * @return bool |
1055 | */ |
1056 | #[Pure] |
1057 | public function isForm() : bool |
1058 | { |
1059 | return $this->parseContentType() === 'application/x-www-form-urlencoded'; |
1060 | } |
1061 | |
1062 | /** |
1063 | * Say if the request is a JSON call. |
1064 | * |
1065 | * @return bool |
1066 | */ |
1067 | #[Pure] |
1068 | public function isJson() : bool |
1069 | { |
1070 | return $this->parseContentType() === 'application/json'; |
1071 | } |
1072 | |
1073 | /** |
1074 | * Say if the request method is POST. |
1075 | * |
1076 | * @return bool |
1077 | */ |
1078 | #[Pure] |
1079 | public function isPost() : bool |
1080 | { |
1081 | return $this->getMethod() === Method::POST; |
1082 | } |
1083 | |
1084 | /** |
1085 | * @see https://www.sitepoint.com/community/t/-files-array-structure/2728/5 |
1086 | * |
1087 | * @return array<string,array<mixed>|UploadedFile> |
1088 | */ |
1089 | protected function getInputFiles() : array |
1090 | { |
1091 | if (empty($_FILES)) { |
1092 | return []; |
1093 | } |
1094 | $makeObjects = static function ( |
1095 | array $array, |
1096 | callable $makeObjects |
1097 | ) : array | UploadedFile { |
1098 | $return = []; |
1099 | foreach ($array as $k => $v) { |
1100 | if (\is_array($v)) { |
1101 | $return[$k] = $makeObjects($v, $makeObjects); |
1102 | continue; |
1103 | } |
1104 | return new UploadedFile($array); |
1105 | } |
1106 | return $return; |
1107 | }; |
1108 | return $makeObjects(ArraySimple::files(), $makeObjects); // @phpstan-ignore-line |
1109 | } |
1110 | |
1111 | /** |
1112 | * @param string $host |
1113 | * |
1114 | * @throws InvalidArgumentException for invalid host |
1115 | * |
1116 | * @return static |
1117 | */ |
1118 | protected function setHost(string $host) : static |
1119 | { |
1120 | $filteredHost = 'http://' . $host; |
1121 | $filteredHost = \filter_var($filteredHost, \FILTER_VALIDATE_URL); |
1122 | if ( ! $filteredHost) { |
1123 | throw new InvalidArgumentException("Invalid host: {$host}"); |
1124 | } |
1125 | $host = \parse_url($filteredHost); |
1126 | $this->host = $host['host']; // @phpstan-ignore-line |
1127 | if (isset($host['port'])) { |
1128 | $this->port = $host['port']; |
1129 | } |
1130 | return $this; |
1131 | } |
1132 | } |