Passed
Push — developer ( 748bda...0a7e9b )
by Mariusz
49:14 queued 14:14
created

Headers::send()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 4
nc 5
nop 0
1
<?php
2
/**
3
 * Headers controller file.
4
 *
5
 * @package Controller
6
 *
7
 * @copyright YetiForce Sp. z o.o
8
 * @license   YetiForce Public License 3.0 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 */
11
12
namespace App\Controller;
13
14
/**
15
 * Headers controller class.
16
 */
17
class Headers
18
{
19
	/**
20
	 * Default header values.
21
	 *
22
	 * @var string[]
23
	 */
24
	protected $headers = [
25
		'Access-Control-Allow-Methods' => 'GET, POST',
26
		'Access-Control-Allow-Origin' => '*',
27
		'Expires' => '-',
28
		'Last-Modified' => '-',
29
		'Pragma' => 'no-cache',
30
		'Cache-Control' => 'private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0',
31
		'Content-Type' => 'text/html; charset=UTF-8',
32
		'Referrer-Policy' => 'no-referrer',
33
		'Permissions-Policy' => 'fullscreen=(self), camera=(), geolocation=()',
34
		'Cross-Origin-Embedder-Policy' => 'require-corp',
35
		'Cross-Origin-Opener-Policy: ' => 'same-origin',
36
		'Cross-Origin-Resource-Policy: ' => 'same-origin',
37
		'Expect-Ct' => 'enforce; max-age=3600',
38
		'X-Frame-Options' => 'sameorigin',
39
		'X-Xss-Protection' => '1; mode=block',
40
		'X-Content-Type-Options' => 'nosniff',
41
		'X-Robots-Tag' => 'none',
42
		'X-Permitted-Cross-Domain-Policies' => 'none',
43
	];
44
	/**
45
	 * Default CSP header values.
46
	 *
47
	 * @var string[]
48
	 */
49
	public $csp = [
50
		'default-src' => '\'self\' blob:',
51
		'img-src' => '\'self\' data:',
52
		'script-src' => '\'self\' \'unsafe-inline\' blob:',
53
		'form-action' => '\'self\'',
54
		'frame-ancestors' => '\'self\'',
55
		'frame-src' => '\'self\' mailto: tel:',
56
		'style-src' => '\'self\' \'unsafe-inline\'',
57
		'connect-src' => '\'self\'',
58
	];
59
	/**
60
	 * Headers to delete.
61
	 *
62
	 * @var string[]
63
	 */
64
	protected $headersToDelete = ['X-Powered-By', 'Server'];
65
66
	/**
67
	 * Headers instance..
68
	 *
69
	 * @var self
70
	 */
71
	public static $instance;
72
73
	/**
74
	 * Get headers instance.
75
	 *
76
	 * @return \self
0 ignored issues
show
Documentation introduced by
Should the return type not be Headers?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
77
	 */
78
	public static function getInstance()
79
	{
80
		if (isset(self::$instance)) {
81
			return self::$instance;
82
		}
83
		if (!\App\Session::get('CSP_TOKEN')) {
84
			self::generateCspToken();
85
		}
86
		return self::$instance = new self();
87
	}
88
89
	/**
90
	 * Construct, loads default headers depending on the browser and environment.
91
	 */
92
	public function __construct()
93
	{
94
		$browser = \App\RequestUtil::getBrowserInfo();
95
		$this->headers['Expires'] = gmdate('D, d M Y H:i:s') . ' GMT';
96
		$this->headers['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
97
		if ($browser->ie) {
98
			$this->headers['X-Ua-Compatible'] = 'IE=11,edge';
99
			if ($browser->https) {
100
				$this->headers['Pragma'] = 'private';
101
				$this->headers['Cache-Control'] = 'private, must-revalidate';
102
			}
103
		}
104
		if ($browser->https) {
105
			$this->headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
106
		}
107
		if (\App\Config::$cspHeaderActive ?? false) {
108
			$this->loadCsp();
109
		}
110
		if ($keys = (\App\Config::$hpkpKeysHeader ?? [])) {
111
			$this->headers['Public-Key-Pins'] = 'pin-sha256="' . implode('"; pin-sha256="', $keys) . '"; max-age=10000;';
112
		}
113
		if ($nonce = \App\Session::get('CSP_TOKEN')) {
114
			$this->csp['script-src'] .= " 'nonce-{$nonce}'";
115
		}
116
		$this->headers['Access-Control-Allow-Origin'] = \App\Config::$portalUrl;
117
	}
118
119
	/**
120
	 * Set header.
121
	 *
122
	 * @param string $key
123
	 * @param string $value
124
	 */
125
	public function setHeader(string $key, string $value)
126
	{
127
		$this->headers[$key] = $value;
128
	}
129
130
	/**
131
	 * Send headers.
132
	 *
133
	 * @return void
134
	 */
135
	public function send(): void
136
	{
137
		if (headers_sent()) {
138
			return;
139
		}
140
		foreach ($this->getHeaders() as $value) {
141
			header($value);
142
		}
143
		foreach ($this->headersToDelete as $name) {
144
			header_remove($name);
145
		}
146
	}
147
148
	/**
149
	 * Get headers string.
150
	 *
151
	 * @return string[]
152
	 */
153
	public function getHeaders(): array
154
	{
155
		if (\App\Config::$cspHeaderActive ?? false) {
156
			$this->headers['Content-Security-Policy'] = $this->getCspHeader();
157
		}
158
		$return = [];
159
		foreach ($this->headers as $name => $value) {
160
			$return[] = "$name: $value";
161
		}
162
		return $return;
163
	}
164
165
	/**
166
	 * Load CSP directive.
167
	 *
168
	 * @return void
169
	 */
170
	public function loadCsp()
171
	{
172
	}
173
174
	/**
175
	 * Get CSP headers string.
176
	 *
177
	 * @return string
178
	 */
179
	public function getCspHeader(): string
180
	{
181
		$scp = '';
182
		foreach ($this->csp as $key => $value) {
183
			$scp .= "$key $value; ";
184
		}
185
		return $scp;
186
	}
187
188
	/**
189
	 * Generate Content Security Policy token.
190
	 *
191
	 * @return void
192
	 */
193
	public static function generateCspToken(): void
194
	{
195
		\App\Session::set('CSP_TOKEN', \base64_encode(\random_bytes(16)));
196
	}
197
}
198