1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the Koded package. |
||
5 | * |
||
6 | * (c) Mihail Binev <[email protected]> |
||
7 | * |
||
8 | * Please view the LICENSE distributed with this source code |
||
9 | * for the full copyright and license information. |
||
10 | * |
||
11 | */ |
||
12 | |||
13 | namespace Koded\Http; |
||
14 | |||
15 | use InvalidArgumentException; |
||
16 | use Koded\Http\Interfaces\Request; |
||
17 | use Psr\Http\Message\ServerRequestInterface; |
||
18 | |||
19 | |||
20 | class ServerRequest extends ClientRequest implements Request |
||
21 | { |
||
22 | use CookieTrait, FilesTrait, ValidatableTrait; |
||
23 | |||
24 | protected string $serverSoftware = ''; |
||
25 | protected array $attributes = []; |
||
26 | protected array $queryParams = []; |
||
27 | |||
28 | protected object|array|null $parsedBody = null; |
||
29 | |||
30 | /** |
||
31 | * ServerRequest constructor. |
||
32 | * |
||
33 | * @param array $attributes |
||
34 | */ |
||
35 | public function __construct(array $attributes = []) |
||
36 | { |
||
37 | parent::__construct($_SERVER['REQUEST_METHOD'] ?? Request::GET, $this->buildUri()); |
||
38 | $this->attributes = $attributes; |
||
39 | $this->extractHttpHeaders($_SERVER); |
||
40 | $this->extractServerData($_SERVER); |
||
41 | } |
||
42 | |||
43 | public function getServerParams(): array |
||
44 | { |
||
45 | return $_SERVER; |
||
46 | } |
||
47 | |||
48 | public function getQueryParams(): array |
||
49 | { |
||
50 | return $this->queryParams; |
||
51 | } |
||
52 | |||
53 | public function withQueryParams(array $query): ServerRequest |
||
54 | { |
||
55 | $instance = clone $this; |
||
56 | $instance->queryParams = array_merge($instance->queryParams, $query); |
||
57 | return $instance; |
||
58 | } |
||
59 | |||
60 | public function getParsedBody() |
||
61 | { |
||
62 | if ($this->useOnlyPost()) { |
||
63 | return $_POST; |
||
64 | } |
||
65 | if (false === empty($_POST)) { |
||
66 | return $_POST; |
||
67 | } |
||
68 | return $this->parsedBody; |
||
69 | } |
||
70 | |||
71 | public function withParsedBody($data): ServerRequest |
||
72 | { |
||
73 | $instance = clone $this; |
||
74 | if ($this->useOnlyPost()) { |
||
75 | $instance->parsedBody = $_POST; |
||
76 | return $instance; |
||
77 | } |
||
78 | // If nothing is available for the body |
||
79 | if (null === $data) { |
||
80 | $instance->parsedBody = null; |
||
81 | return $instance; |
||
82 | } |
||
83 | // Supports array or iterable object |
||
84 | if (is_iterable($data)) { |
||
85 | $instance->parsedBody = is_array($data) ? $data : iterator_to_array($data); |
||
86 | return $instance; |
||
87 | } |
||
88 | if (is_object($data)) { |
||
89 | $instance->parsedBody = $data; |
||
90 | return $instance; |
||
91 | } |
||
92 | throw new InvalidArgumentException( |
||
93 | sprintf('Unsupported data provided (%s), Expects NULL, array or iterable', gettype($data)) |
||
94 | ); |
||
95 | } |
||
96 | |||
97 | public function getAttributes(): array |
||
98 | { |
||
99 | return $this->attributes; |
||
100 | } |
||
101 | |||
102 | public function getAttribute($name, $default = null) |
||
103 | { |
||
104 | return $this->attributes[$name] ?? $default; |
||
105 | } |
||
106 | |||
107 | public function withAttribute($name, $value): ServerRequest |
||
108 | { |
||
109 | $instance = clone $this; |
||
110 | $instance->attributes[$name] = $value; |
||
111 | return $instance; |
||
112 | } |
||
113 | |||
114 | public function withoutAttribute($name): ServerRequest |
||
115 | { |
||
116 | $instance = clone $this; |
||
117 | unset($instance->attributes[$name]); |
||
118 | return $instance; |
||
119 | } |
||
120 | |||
121 | public function withAttributes(array $attributes): Request |
||
122 | { |
||
123 | $instance = clone $this; |
||
124 | foreach ($attributes as $name => $value) { |
||
125 | $instance->attributes[$name] = $value; |
||
126 | } |
||
127 | return $instance; |
||
128 | } |
||
129 | |||
130 | public function isXHR(): bool |
||
131 | { |
||
132 | return 'XMLHTTPREQUEST' === strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''); |
||
133 | } |
||
134 | |||
135 | protected function buildUri(): Uri |
||
136 | { |
||
137 | if (strpos($_SERVER['REQUEST_URI'] ?? '', '://')) { |
||
138 | return new Uri($_SERVER['REQUEST_URI']); |
||
139 | } |
||
140 | if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') { |
||
141 | return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '') |
||
142 | . '://' . $host |
||
143 | . ':' . ($_SERVER['SERVER_PORT'] ?? 80) |
||
144 | . ($_SERVER['REQUEST_URI'] ?? '') |
||
145 | ); |
||
146 | } |
||
147 | return new Uri($_SERVER['REQUEST_URI'] ?? ''); |
||
148 | } |
||
149 | |||
150 | protected function extractHttpHeaders(array $server): void |
||
151 | { |
||
152 | foreach ($server as $k => $v) { |
||
153 | // Calisthenics :) |
||
154 | 0 === strpos($k, 'HTTP_', 0) && $this->normalizeHeader(str_replace('HTTP_', '', $k), $v, false); |
||
155 | } |
||
156 | unset($this->headers['X-Forwarded-For'], $this->headers['X-Forwarded-Proto']); |
||
157 | unset($this->headersMap['x-forwarded-for'], $this->headersMap['x-forwarded-proto']); |
||
158 | if (isset($server['HTTP_IF_NONE_MATCH'])) { |
||
159 | // ETag workaround for various broken Apache2 versions |
||
160 | $this->headers['ETag'] = str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']); |
||
161 | $this->headersMap['etag'] = 'ETag'; |
||
162 | } |
||
163 | if (isset($server['CONTENT_TYPE'])) { |
||
164 | $this->headers['Content-Type'] = strtolower($server['CONTENT_TYPE']); |
||
165 | $this->headersMap['content-type'] = 'Content-Type'; |
||
166 | } |
||
167 | $this->setHost(); |
||
168 | } |
||
169 | |||
170 | protected function extractServerData(array $server): void |
||
171 | { |
||
172 | $this->protocolVersion = str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion); |
||
0 ignored issues
–
show
|
|||
173 | $this->serverSoftware = $server['SERVER_SOFTWARE'] ?? ''; |
||
174 | $this->queryParams = $_GET; |
||
175 | $this->cookieParams = $_COOKIE; |
||
176 | if (false === $this->isSafeMethod()) { |
||
177 | $this->parseInput(); |
||
178 | } |
||
179 | if ($_FILES) { |
||
180 | $this->uploadedFiles = $this->parseUploadedFiles($_FILES); |
||
181 | } |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Per recommendation: |
||
186 | * |
||
187 | * @return bool If the request Content-Type is either |
||
188 | * application/x-www-form-urlencoded or multipart/form-data |
||
189 | * and the request method is POST, |
||
190 | * then it MUST return the contents of $_POST |
||
191 | * @see ServerRequestInterface::withParsedBody() |
||
192 | * |
||
193 | * @see ServerRequestInterface::getParsedBody() |
||
194 | */ |
||
195 | protected function useOnlyPost(): bool |
||
196 | { |
||
197 | if (empty($contentType = $this->getHeaderLine('Content-Type'))) { |
||
198 | return false; |
||
199 | } |
||
200 | return $this->method === self::POST && ( |
||
201 | false !== strpos('application/x-www-form-urlencoded', $contentType) || |
||
202 | false !== strpos('multipart/form-data', $contentType)); |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * Try to unserialize a JSON string or form encoded request body. |
||
207 | * Very useful if JavaScript app stringify objects in AJAX requests. |
||
208 | */ |
||
209 | protected function parseInput(): void |
||
210 | { |
||
211 | if (empty($input = $this->getRawInput())) { |
||
212 | return; |
||
213 | } |
||
214 | // Try JSON deserialization |
||
215 | $this->parsedBody = json_decode($input, true, 512, JSON_BIGINT_AS_STRING); |
||
216 | if (null === $this->parsedBody) { |
||
217 | parse_str($input, $this->parsedBody); |
||
218 | } |
||
219 | } |
||
220 | |||
221 | protected function getRawInput(): string |
||
222 | { |
||
223 | return file_get_contents('php://input') ?: ''; |
||
224 | } |
||
225 | } |
||
226 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.