Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.14% |
279 / 313 |
|
60.00% |
9 / 15 |
CRAP | |
0.00% |
0 / 1 |
HttpHeader | |
89.14% |
279 / 313 |
|
60.00% |
9 / 15 |
124.23 | |
0.00% |
0 / 1 |
initCurrentRequest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
set | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
setDownloadable | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
isSecurityHeader | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
getProtocolVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReferer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequestTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequestIp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBrowserName | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
110 | |||
getAllHeaders | |
36.00% |
9 / 25 |
|
0.00% |
0 / 1 |
24.78 | |||
remove | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getReasonPhrase | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
has | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
push | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
generate | |
100.00% |
236 / 236 |
|
100.00% |
1 / 1 |
61 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\Message\Http |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Message\Http; |
16 | |
17 | use phpOMS\Message\HeaderAbstract; |
18 | use phpOMS\System\MimeType; |
19 | |
20 | /** |
21 | * Response class. |
22 | * |
23 | * @package phpOMS\Message\Http |
24 | * @license OMS License 2.0 |
25 | * @link https://jingga.app |
26 | * @since 1.0.0 |
27 | * |
28 | * @SuppressWarnings(PHPMD.Superglobals) |
29 | */ |
30 | final class HttpHeader extends HeaderAbstract |
31 | { |
32 | /** |
33 | * Header. |
34 | * |
35 | * @var string[][] |
36 | * @since 1.0.0 |
37 | */ |
38 | private array $header = []; |
39 | |
40 | /** |
41 | * Server headers. |
42 | * |
43 | * @var array |
44 | * @since 1.0.0 |
45 | */ |
46 | private static $serverHeaders = []; |
47 | |
48 | /** |
49 | * Response status. |
50 | * |
51 | * @var int |
52 | * @since 1.0.0 |
53 | */ |
54 | public int $status = RequestStatusCode::R_200; |
55 | |
56 | /** |
57 | * Init header from current request. |
58 | * |
59 | * @return void |
60 | * |
61 | * @since 1.0.0 |
62 | */ |
63 | public function initCurrentRequest() : void |
64 | { |
65 | $this->header = self::getAllHeaders(); |
66 | } |
67 | |
68 | /** |
69 | * {@inheritdoc} |
70 | */ |
71 | public function set(string $key, string $header, bool $overwrite = false) : bool |
72 | { |
73 | if ($this->isLocked) { |
74 | return false; |
75 | } |
76 | |
77 | $key = \strtolower($key); |
78 | $exists = isset($this->header[$key]); |
79 | |
80 | if ($exists && self::isSecurityHeader($key)) { |
81 | return false; |
82 | } |
83 | |
84 | if ($exists && $overwrite) { |
85 | unset($this->header[$key]); |
86 | $exists = false; |
87 | } |
88 | |
89 | if (!$exists) { |
90 | $this->header[$key] = []; |
91 | } |
92 | |
93 | $this->header[$key][] = $header; |
94 | |
95 | return true; |
96 | } |
97 | |
98 | /** |
99 | * Set header as downloadable |
100 | * |
101 | * @param string $name Download name |
102 | * @param string $type Download file type |
103 | * |
104 | * @return void |
105 | * |
106 | * @since 1.0.0 |
107 | */ |
108 | public function setDownloadable(string $name, string $type) : void |
109 | { |
110 | $this->set('Content-Type', MimeType::M_BIN, true); |
111 | $this->set('Content-Transfer-Encoding', 'Binary', true); |
112 | $this->set( |
113 | 'Content-disposition', 'attachment; filename="' . $name . '.' . $type . '"' |
114 | , true); |
115 | } |
116 | |
117 | /** |
118 | * Is security header. |
119 | * |
120 | * @param string $key Header key |
121 | * |
122 | * @return bool |
123 | * |
124 | * @since 1.0.0 |
125 | */ |
126 | public static function isSecurityHeader(string $key) : bool |
127 | { |
128 | $key = \strtolower($key); |
129 | |
130 | return $key === 'content-security-policy' |
131 | || $key === 'x-xss-protection' |
132 | || $key === 'x-content-type-options' |
133 | || $key === 'x-frame-options'; |
134 | } |
135 | |
136 | /** |
137 | * {@inheritdoc} |
138 | */ |
139 | public function getProtocolVersion() : string |
140 | { |
141 | return $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; |
142 | } |
143 | |
144 | /** |
145 | * Get the referer link |
146 | * |
147 | * @return string |
148 | * |
149 | * @since 1.0.0 |
150 | */ |
151 | public function getReferer() : string |
152 | { |
153 | return $_SERVER['HTTP_REFERER'] ?? ''; |
154 | } |
155 | |
156 | /** |
157 | * {@inheritdoc} |
158 | */ |
159 | public function getRequestTime() : int |
160 | { |
161 | return (int) ($_SERVER['REQUEST_TIME'] ?? $this->timestamp); |
162 | } |
163 | |
164 | /** |
165 | * Get the ip of the requester |
166 | * |
167 | * @return string |
168 | * |
169 | * @since 1.0.0 |
170 | */ |
171 | public function getRequestIp() : string |
172 | { |
173 | return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; |
174 | } |
175 | |
176 | /** |
177 | * Get the browser/agent name of the request |
178 | * |
179 | * @return string |
180 | * |
181 | * @since 1.0.0 |
182 | */ |
183 | public function getBrowserName() : string |
184 | { |
185 | $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; |
186 | |
187 | if (\strpos($userAgent, 'Opera') !== false || \strpos($userAgent, 'OPR/') !== false) { |
188 | return 'Opera'; |
189 | } elseif (\strpos($userAgent, 'Edge') !== false || \strpos($userAgent, 'Edg/') !== false) { |
190 | return 'Microsoft Edge'; |
191 | } elseif (\strpos($userAgent, 'Chrome') !== false) { |
192 | return 'Google Chrome'; |
193 | } elseif (\strpos($userAgent, 'Safari') !== false) { |
194 | return 'Safari'; |
195 | } elseif (\strpos($userAgent, 'Firefox') !== false) { |
196 | return 'Mozilla Firefox'; |
197 | } elseif (\strpos($userAgent, 'MSIE') !== false || \strpos($userAgent, 'Trident/7') !== false) { |
198 | return 'Internet Explorer'; |
199 | } |
200 | |
201 | return 'Unknown'; |
202 | } |
203 | |
204 | /** |
205 | * Get all headers for apache and nginx |
206 | * |
207 | * @return array |
208 | * |
209 | * @since 1.0.0 |
210 | */ |
211 | public static function getAllHeaders() : array |
212 | { |
213 | if (!empty(self::$serverHeaders)) { |
214 | return self::$serverHeaders; |
215 | } |
216 | |
217 | if (\function_exists('getallheaders')) { |
218 | // @codeCoverageIgnoreStart |
219 | self::$serverHeaders = \getallheaders(); |
220 | // @codeCoverageIgnoreEnd |
221 | } |
222 | |
223 | foreach ($_SERVER as $name => $value) { |
224 | $part = \substr($name, 0, 5); |
225 | if ($part === 'HTTP_') { |
226 | self::$serverHeaders[ |
227 | \strtr( |
228 | \strtolower( |
229 | \strtr(\substr($name, 5), '_', ' ') |
230 | ), |
231 | ' ', |
232 | '-' |
233 | ) |
234 | ] = $value; |
235 | } |
236 | } |
237 | |
238 | $temp = []; |
239 | foreach (self::$serverHeaders as $key => $value) { |
240 | $key = \strtolower($key); |
241 | if (!isset($temp[$key])) { |
242 | $temp[$key] = []; |
243 | } |
244 | |
245 | $values = \explode(',', $value); |
246 | foreach ($values as $val) { |
247 | $temp[$key][] = \trim($val); |
248 | } |
249 | } |
250 | |
251 | self::$serverHeaders = $temp; |
252 | |
253 | return self::$serverHeaders; |
254 | } |
255 | |
256 | /** |
257 | * Remove header by ID. |
258 | * |
259 | * @param string $key Header key |
260 | * |
261 | * @return bool |
262 | * |
263 | * @since 1.0.0 |
264 | */ |
265 | public function remove(string $key) : bool |
266 | { |
267 | if ($this->isLocked) { |
268 | return false; |
269 | } |
270 | |
271 | if (isset($this->header[$key])) { |
272 | unset($this->header[$key]); |
273 | |
274 | return true; |
275 | } |
276 | |
277 | return false; |
278 | } |
279 | |
280 | /** |
281 | * {@inheritdoc} |
282 | */ |
283 | public function getReasonPhrase() : string |
284 | { |
285 | $phrases = $this->get('Status'); |
286 | |
287 | return empty($phrases) ? '' : $phrases[0]; |
288 | } |
289 | |
290 | /** |
291 | * {@inheritdoc} |
292 | */ |
293 | public function get(string $key = null) : array |
294 | { |
295 | return $key === null ? $this->header : ($this->header[\strtolower($key)] ?? []); |
296 | } |
297 | |
298 | /** |
299 | * {@inheritdoc} |
300 | */ |
301 | public function has(string $key) : bool |
302 | { |
303 | return isset($this->header[$key]); |
304 | } |
305 | |
306 | /** |
307 | * Push all headers. |
308 | * |
309 | * @return void |
310 | * |
311 | * @throws \Exception |
312 | * |
313 | * @since 1.0.0 |
314 | * @codeCoverageIgnore |
315 | */ |
316 | public function push() : void |
317 | { |
318 | if ($this->isLocked) { |
319 | throw new \Exception('Already locked'); |
320 | } |
321 | |
322 | $this->generate($this->status); |
323 | |
324 | foreach ($this->header as $name => $arr) { |
325 | if (empty($name)) { |
326 | foreach ($arr as $value) { |
327 | \header($value); |
328 | } |
329 | } else { |
330 | \header($name . ': ' . \implode(';', $arr)); |
331 | } |
332 | } |
333 | |
334 | \header("X-Powered-By: hidden"); |
335 | |
336 | $this->lock(); |
337 | } |
338 | |
339 | /** |
340 | * {@inheritdoc} |
341 | */ |
342 | public function generate(int $code) : void |
343 | { |
344 | switch ($code) { |
345 | case RequestStatusCode::R_100: |
346 | $this->set('', 'HTTP/1.0 100 Continue', true); |
347 | $this->set('Status', '100 Continue', true); |
348 | break; |
349 | case RequestStatusCode::R_101: |
350 | $this->set('', 'HTTP/1.0 101 Switching protocols', true); |
351 | $this->set('Status', '101 Switching protocols', true); |
352 | break; |
353 | case RequestStatusCode::R_102: |
354 | $this->set('', 'HTTP/1.0 102 Processing', true); |
355 | $this->set('Status', '102 Processing', true); |
356 | break; |
357 | case RequestStatusCode::R_200: |
358 | $this->set('', 'HTTP/1.0 200 OK', true); |
359 | $this->set('Status', '200 OK', true); |
360 | break; |
361 | case RequestStatusCode::R_201: |
362 | $this->set('', 'HTTP/1.0 201 Created', true); |
363 | $this->set('Status', '201 Created', true); |
364 | break; |
365 | case RequestStatusCode::R_202: |
366 | $this->set('', 'HTTP/1.0 202 Accepted', true); |
367 | $this->set('Status', '202 Accepted', true); |
368 | break; |
369 | case RequestStatusCode::R_203: |
370 | $this->set('', 'HTTP/1.0 203 Non-Authoritative Information', true); |
371 | $this->set('Status', '203 Non-Authoritative Information', true); |
372 | break; |
373 | case RequestStatusCode::R_204: |
374 | $this->set('', 'HTTP/1.0 204 No Content', true); |
375 | $this->set('Status', '204 No Content', true); |
376 | break; |
377 | case RequestStatusCode::R_205: |
378 | $this->set('', 'HTTP/1.0 205 Reset Content', true); |
379 | $this->set('Status', '205 Reset Content', true); |
380 | break; |
381 | case RequestStatusCode::R_206: |
382 | $this->set('', 'HTTP/1.0 206 Partial Content', true); |
383 | $this->set('Status', '206 Partial Content', true); |
384 | break; |
385 | case RequestStatusCode::R_207: |
386 | $this->set('', 'HTTP/1.0 207 Multi-Status', true); |
387 | $this->set('Status', '207 Multi-Status', true); |
388 | break; |
389 | case RequestStatusCode::R_300: |
390 | $this->set('', 'HTTP/1.0 300 Multiple Choices', true); |
391 | $this->set('Status', '300 Multiple Choices', true); |
392 | break; |
393 | case RequestStatusCode::R_301: |
394 | $this->set('', 'HTTP/1.0 301 Moved Permanently', true); |
395 | $this->set('Status', '301 Moved Permanently', true); |
396 | break; |
397 | case RequestStatusCode::R_302: |
398 | $this->set('', 'HTTP/1.0 302 Found', true); |
399 | $this->set('Status', '302 Found', true); |
400 | break; |
401 | case RequestStatusCode::R_303: |
402 | $this->set('', 'HTTP/1.0 303 See Other', true); |
403 | $this->set('Status', '303 See Other', true); |
404 | break; |
405 | case RequestStatusCode::R_304: |
406 | $this->set('', 'HTTP/1.0 304 Not Modified', true); |
407 | $this->set('Status', '304 Not Modified', true); |
408 | break; |
409 | case RequestStatusCode::R_305: |
410 | $this->set('', 'HTTP/1.0 305 Use Proxy', true); |
411 | $this->set('Status', '305 Use Proxy', true); |
412 | break; |
413 | case RequestStatusCode::R_306: |
414 | $this->set('', 'HTTP/1.0 306 Switch Proxy', true); |
415 | $this->set('Status', '306 Switch Proxy', true); |
416 | break; |
417 | case RequestStatusCode::R_307: |
418 | $this->set('', 'HTTP/1.0 307 Temporary Redirect', true); |
419 | $this->set('Status', '307 Temporary Redirect', true); |
420 | break; |
421 | case RequestStatusCode::R_308: |
422 | $this->set('', 'HTTP/1.0 308 Permanent Redirect', true); |
423 | $this->set('Status', '308 Permanent Redirect', true); |
424 | break; |
425 | case RequestStatusCode::R_400: |
426 | $this->set('', 'HTTP/1.0 400 Bad Request', true); |
427 | $this->set('Status', '400 Bad Request', true); |
428 | break; |
429 | case RequestStatusCode::R_401: |
430 | $this->set('', 'HTTP/1.0 401 Unauthorized', true); |
431 | $this->set('Status', '401 Unauthorized', true); |
432 | break; |
433 | case RequestStatusCode::R_402: |
434 | $this->set('', 'HTTP/1.0 402 Payment Required', true); |
435 | $this->set('Status', '402 Payment Required', true); |
436 | break; |
437 | case RequestStatusCode::R_403: |
438 | $this->set('', 'HTTP/1.0 403 Forbidden', true); |
439 | $this->set('Status', '403 Forbidden', true); |
440 | break; |
441 | case RequestStatusCode::R_404: |
442 | $this->set('', 'HTTP/1.0 404 Not Found', true); |
443 | $this->set('Status', '404 Not Found', true); |
444 | break; |
445 | case RequestStatusCode::R_405: |
446 | $this->set('', 'HTTP/1.0 405 Method Not Allowed', true); |
447 | $this->set('Status', '405 Method Not Allowed', true); |
448 | break; |
449 | case RequestStatusCode::R_406: |
450 | $this->set('', 'HTTP/1.0 406 Not acceptable', true); |
451 | $this->set('Status', '406 Not acceptable', true); |
452 | break; |
453 | case RequestStatusCode::R_407: |
454 | $this->set('', 'HTTP/1.0 407 Proxy Authentication Required', true); |
455 | $this->set('Status', '407 Proxy Authentication Required', true); |
456 | break; |
457 | case RequestStatusCode::R_408: |
458 | $this->set('', 'HTTP/1.0 408 Request Timeout', true); |
459 | $this->set('Status', '408 Request Timeout', true); |
460 | break; |
461 | case RequestStatusCode::R_409: |
462 | $this->set('', 'HTTP/1.0 409 Conflict', true); |
463 | $this->set('Status', '409 Conflict', true); |
464 | break; |
465 | case RequestStatusCode::R_410: |
466 | $this->set('', 'HTTP/1.0 410 Gone', true); |
467 | $this->set('Status', '410 Gone', true); |
468 | break; |
469 | case RequestStatusCode::R_411: |
470 | $this->set('', 'HTTP/1.0 411 Length Required', true); |
471 | $this->set('Status', '411 Length Required', true); |
472 | break; |
473 | case RequestStatusCode::R_412: |
474 | $this->set('', 'HTTP/1.0 412 Precondition Failed', true); |
475 | $this->set('Status', '412 Precondition Failed', true); |
476 | break; |
477 | case RequestStatusCode::R_413: |
478 | $this->set('', 'HTTP/1.0 413 Request Entity Too Large', true); |
479 | $this->set('Status', '413 Request Entity Too Large', true); |
480 | break; |
481 | case RequestStatusCode::R_414: |
482 | $this->set('', 'HTTP/1.0 414 Request-URI Too Long', true); |
483 | $this->set('Status', '414 Request-URI Too Long', true); |
484 | break; |
485 | case RequestStatusCode::R_415: |
486 | $this->set('', 'HTTP/1.0 415 Unsupported Media Type', true); |
487 | $this->set('Status', '415 Unsupported Media Type', true); |
488 | break; |
489 | case RequestStatusCode::R_416: |
490 | $this->set('', 'HTTP/1.0 416 Requested Range Not Satisfiable', true); |
491 | $this->set('Status', '416 Requested Range Not Satisfiable', true); |
492 | break; |
493 | case RequestStatusCode::R_417: |
494 | $this->set('', 'HTTP/1.0 417 Expectation Failed', true); |
495 | $this->set('Status', '417 Expectation Failed', true); |
496 | break; |
497 | case RequestStatusCode::R_421: |
498 | $this->set('', 'HTTP/1.0 421 Misdirected Request', true); |
499 | $this->set('Status', '421 Misdirected Request', true); |
500 | break; |
501 | case RequestStatusCode::R_422: |
502 | $this->set('', 'HTTP/1.0 422 Unprocessable Entity', true); |
503 | $this->set('Status', '422 Unprocessable Entity', true); |
504 | break; |
505 | case RequestStatusCode::R_423: |
506 | $this->set('', 'HTTP/1.0 423 Locked', true); |
507 | $this->set('Status', '423 Locked', true); |
508 | break; |
509 | case RequestStatusCode::R_424: |
510 | $this->set('', 'HTTP/1.0 424 Failed Dependency', true); |
511 | $this->set('Status', '424 Failed Dependency', true); |
512 | break; |
513 | case RequestStatusCode::R_425: |
514 | $this->set('', 'HTTP/1.0 425 Too Early', true); |
515 | $this->set('Status', '425 Too Early', true); |
516 | break; |
517 | case RequestStatusCode::R_426: |
518 | $this->set('', 'HTTP/1.0 426 Upgrade Required', true); |
519 | $this->set('Status', '426 Upgrade Required', true); |
520 | break; |
521 | case RequestStatusCode::R_428: |
522 | $this->set('', 'HTTP/1.0 428 Precondition Required', true); |
523 | $this->set('Status', '428 Precondition Required', true); |
524 | break; |
525 | case RequestStatusCode::R_429: |
526 | $this->set('', 'HTTP/1.0 429 Too Many Requests', true); |
527 | $this->set('Status', '429 Too Many Requests', true); |
528 | break; |
529 | case RequestStatusCode::R_431: |
530 | $this->set('', 'HTTP/1.0 431 Request Header Fields Too Large', true); |
531 | $this->set('Status', '431 Request Header Fields Too Large', true); |
532 | break; |
533 | case RequestStatusCode::R_451: |
534 | $this->set('', 'HTTP/1.0 451 Unavailable For Legal Reasons', true); |
535 | $this->set('Status', '451 Unavailable For Legal Reasons', true); |
536 | break; |
537 | case RequestStatusCode::R_501: |
538 | $this->set('', 'HTTP/1.0 501 Not Implemented', true); |
539 | $this->set('Status', '501 Not Implemented', true); |
540 | break; |
541 | case RequestStatusCode::R_502: |
542 | $this->set('', 'HTTP/1.0 502 Bad Gateway', true); |
543 | $this->set('Status', '502 Bad Gateway', true); |
544 | break; |
545 | case RequestStatusCode::R_503: |
546 | $this->set('', 'HTTP/1.0 503 Service Temporarily Unavailable', true); |
547 | $this->set('Status', '503 Service Temporarily Unavailable', true); |
548 | $this->set('Retry-After', 'Retry-After: 300', true); |
549 | break; |
550 | case RequestStatusCode::R_504: |
551 | $this->set('', 'HTTP/1.0 504 Gateway Timeout', true); |
552 | $this->set('Status', '504 Gateway Timeout', true); |
553 | break; |
554 | case RequestStatusCode::R_505: |
555 | $this->set('', 'HTTP/1.0 505 HTTP Version Not Supported', true); |
556 | $this->set('Status', '505 HTTP Version Not Supported', true); |
557 | break; |
558 | case RequestStatusCode::R_506: |
559 | $this->set('', 'HTTP/1.0 506 HTTP Variant Also Negotiates', true); |
560 | $this->set('Status', '506 HTTP Variant Also Negotiates', true); |
561 | break; |
562 | case RequestStatusCode::R_507: |
563 | $this->set('', 'HTTP/1.0 507 Insufficient Storage', true); |
564 | $this->set('Status', '507 Insufficient Storage', true); |
565 | break; |
566 | case RequestStatusCode::R_508: |
567 | $this->set('', 'HTTP/1.0 508 Loop Detected', true); |
568 | $this->set('Status', '508 Loop Detected', true); |
569 | break; |
570 | case RequestStatusCode::R_510: |
571 | $this->set('', 'HTTP/1.0 510 Not Extended', true); |
572 | $this->set('Status', '510 Not Extended', true); |
573 | break; |
574 | case RequestStatusCode::R_511: |
575 | $this->set('', 'HTTP/1.0 511 Network Authentication Required', true); |
576 | $this->set('Status', '511 Network Authentication Required', true); |
577 | break; |
578 | case RequestStatusCode::R_500: |
579 | default: |
580 | $this->set('', 'HTTP/1.0 500 Internal Server Error', true); |
581 | $this->set('Status', '500 Internal Server Error', true); |
582 | } |
583 | } |
584 | } |