Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.14% covered (warning)
89.14%
279 / 313
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpHeader
89.14% covered (warning)
89.14%
279 / 313
60.00% covered (warning)
60.00%
9 / 15
124.23
0.00% covered (danger)
0.00%
0 / 1
 initCurrentRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 setDownloadable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 isSecurityHeader
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getProtocolVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReferer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestIp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBrowserName
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
110
 getAllHeaders
36.00% covered (danger)
36.00%
9 / 25
0.00% covered (danger)
0.00%
0 / 1
24.78
 remove
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getReasonPhrase
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 push
n/a
0 / 0
n/a
0 / 0
5
 generate
100.00% covered (success)
100.00%
236 / 236
100.00% covered (success)
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 */
13declare(strict_types=1);
14
15namespace phpOMS\Message\Http;
16
17use phpOMS\Message\HeaderAbstract;
18use 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 */
30final 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}