Passed
Push — master ( b02c13...e72a08 )
by Jean-Christophe
05:41
created

ContentSecurity::removePolicy()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 7
c 1
b 0
f 1
dl 0
loc 11
rs 10
cc 4
nc 5
nop 2
1
<?php
2
namespace Ubiquity\security\csp;
3
4
use Ubiquity\controllers\Startup;
5
use Ubiquity\utils\http\UResponse;
6
7
/**
8
 * Creates a Content Security Policy object.
9
 * Ubiquity\security\csp$ContentSecurity
10
 * This class is part of Ubiquity
11
 *
12
 * @author jc
13
 * @version 1.0.0
14
 *
15
 */
16
class ContentSecurity {
17
18
	const HEADER = 'Content-Security-Policy';
19
20
	const DEBUG_HEADER = 'Content-Security-Policy-Report-Only';
21
22
	private array $policies = [];
23
24
	private $header = self::HEADER;
25
26
	/**
27
	 * ContentSecurity constructor.
28
	 *
29
	 * @param bool|null $reportOnly
30
	 */
31
	public function __construct(?bool $reportOnly = null) {
32
		if (isset($reportOnly)) {
33
			$this->reportOnly($reportOnly);
34
		}
35
	}
36
37
	/**
38
	 * Adds new values to a directive.
39
	 *
40
	 * @param string $directive
41
	 * @param string ...$values
42
	 * @return $this
43
	 */
44
	public function addPolicy(string $directive, string ...$values): self {
45
		$policies = $this->policies[$directive] ?? [];
46
		foreach ($values as $v) {
47
			if (\in_array($v, CspValues::QUOTED)) {
48
				$v = "'$v'";
49
			}
50
			$policies[$v] = true;
51
		}
52
		$this->policies[$directive] = $policies;
53
		return $this;
54
	}
55
56
	public function removePolicy(string $directive, string ...$values): self {
57
		$policies = $this->policies[$directive] ?? [];
0 ignored issues
show
Unused Code introduced by
The assignment to $policies is dead and can be removed.
Loading history...
58
		foreach ($values as $v) {
59
			if (\in_array($v, CspValues::QUOTED)) {
60
				$v = "'$v'";
61
			}
62
			if (isset($this->policies[$directive][$v])) {
63
				unset($this->policies[$directive][$v]);
64
			}
65
		}
66
		return $this;
67
	}
68
69
	/**
70
	 * Adds new values to a directive, re-using default-src actual values.
71
	 *
72
	 * @param string $directive
73
	 * @param string ...$values
74
	 * @return $this
75
	 */
76
	public function addPolicyDefault(string $directive, string ...$values): self {
77
		$default = \array_keys($this->policies[CspDirectives::DEFAULT_SRC] ?? []);
78
		$values = \array_merge($default, $values);
79
		$this->addPolicy($directive, ...$values);
80
		return $this;
81
	}
82
83
	/**
84
	 * Adds a nonce to the directives.
85
	 *
86
	 * @param string $nonce
87
	 * @param string ...$directives
88
	 * @return $this
89
	 */
90
	public function addNonce(string $nonce, string ...$directives): self {
91
		foreach ($directives as $directive) {
92
			$this->addPolicy($directive, "'nonce-$nonce'", CspValues::STRICT_DYNAMIC);
93
		}
94
		return $this;
95
	}
96
97
	/**
98
	 * Adds a hash to the directives.
99
	 *
100
	 * @param string $hash
101
	 * @param string ...$directives
102
	 * @return $this
103
	 */
104
	public function addHash(string $hash, string ...$directives): self {
105
		foreach ($directives as $directive) {
106
			$this->addPolicy($directive, "'$hash'");
107
		}
108
		return $this;
109
	}
110
111
	/**
112
	 * Generates a hash and add it to a directive.
113
	 *
114
	 * @param string $code
115
	 * @param string $directive
116
	 *        	default script-src
117
	 * @param string $algo
118
	 *        	default sha256, possible value sha384,sha512
119
	 * @return $this
120
	 */
121
	public function generateHash(string $code, string $directive = CspDirectives::SCRIPT_SRC, string $algo = 'sha256'): self {
122
		$code = \preg_replace('/\r\n/', '\n', $code);
123
		$hash = \base64_encode(\hash($algo, $code, true));
124
		return $this->addHash("$algo-$hash", $directive);
125
	}
126
127
	/**
128
	 * Adds a nonce to a directive, re-using default-src actual values.
129
	 *
130
	 * @param string $nonce
131
	 * @param string ...$directives
132
	 * @return $this
133
	 */
134
	public function addNonceDefault(string $nonce, string ...$directives): self {
135
		foreach ($directives as $directive) {
136
			$this->addPolicyDefault($directive, "'nonce-$nonce'", CspValues::STRICT_DYNAMIC);
137
		}
138
		return $this;
139
	}
140
141
	/**
142
	 * Adds a hash to a directive, re-using default-src actual values.
143
	 *
144
	 * @param string $hash
145
	 * @param string ...$directives
146
	 * @return $this
147
	 */
148
	public function addHashDefault(string $hash, string ...$directives): self {
149
		foreach ($directives as $directive) {
150
			$this->addPolicyDefault($directive, "'$hash'");
151
		}
152
		return $this;
153
	}
154
155
	/**
156
	 * Defines the policies for default-src directive.
157
	 *
158
	 * @param string ...$policies
159
	 * @return $this
160
	 */
161
	public function setDefaultSrc(string ...$policies): self {
162
		return $this->addPolicy(CspDirectives::DEFAULT_SRC, ...$policies);
163
	}
164
165
	/**
166
	 * Generates the header string.
167
	 *
168
	 * @return string
169
	 */
170
	public function generate(): string {
171
		$strs = '';
172
		foreach ($this->policies as $directive => $policy) {
173
			$policies = \array_keys($policy);
174
			$strs .= $directive . ' ' . \implode(' ', $policies) . ';';
175
		}
176
		return $strs;
177
	}
178
179
	/**
180
	 * Display a ContentSecurity object.
181
	 *
182
	 * @param callable $directiveCall
183
	 * @param callable $policyCall
184
	 * @return string
185
	 */
186
	public function display(callable $directiveCall, callable $policyCall): string {
187
		$strs = '';
188
		foreach ($this->policies as $directive => $policy) {
189
			$policies = \array_keys($policy);
190
			$strs .= $directiveCall($directive) . $policyCall(\implode(' ', $policies));
191
		}
192
		return $strs;
193
	}
194
195
	/**
196
	 * Sets reportOnly.
197
	 *
198
	 * @param bool|null $reportOnly
199
	 * @return $this
200
	 */
201
	public function reportOnly(?bool $reportOnly = true): self {
202
		if (isset($reportOnly)) {
203
			$this->header = $reportOnly ? self::DEBUG_HEADER : self::HEADER;
204
		}
205
		return $this;
206
	}
207
208
	/**
209
	 * Adds headers to the response.
210
	 *
211
	 * @param bool|null $reportOnly
212
	 */
213
	public function addHeaderToResponse(?bool $reportOnly = null): void {
214
		if (isset($reportOnly)) {
215
			$this->reportOnly($reportOnly);
216
		}
217
		UResponse::header($this->header, $this->generate(), false);
218
	}
219
220
	/**
221
	 * Creates a nonce and add it to some directives.
222
	 *
223
	 * @param
224
	 *        	$nonce
225
	 * @param string ...$directives
226
	 * @return ContentSecurity
227
	 */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $nonce at position 0 could not be parsed: Unknown type name '$nonce' at position 0 in $nonce.
Loading history...
228
	public static function nonce($nonce, string ...$directives): ContentSecurity {
229
		$csp = new self();
230
		return $csp->addNonce($nonce, ...$directives);
231
	}
232
233
	/**
234
	 * Creates a new ContentSecurity object, with self in default-src.
235
	 *
236
	 * @return ContentSecurity
237
	 */
238
	public static function all(): ContentSecurity {
239
		$csp = new self();
240
		return $csp->addPolicy(CspDirectives::DEFAULT_SRC, CspValues::SELF);
241
	}
242
243
	/**
244
	 * Returns the actual policies.
245
	 *
246
	 * @return array
247
	 */
248
	public function getPolicies(): array {
249
		return $this->policies;
250
	}
251
252
	/**
253
	 * Creates a new ContentSecurity object for Ubiquity Webtools.
254
	 *
255
	 * @return ContentSecurity
256
	 */
257
	public static function defaultUbiquity(): ContentSecurity {
258
		$csp = new self();
259
		$csp->addPolicyDefault(CspDirectives::CONNECT_SRC, CspValues::SELF);
260
		$csp->addPolicy(CspDirectives::IMG_SRC, 'data:');
261
		return $csp;
262
	}
263
264
	/**
265
	 * Creates a new ContentSecurity object for Ubiquity Webtools in debug mode.
266
	 *
267
	 * @param string $livereloadServer
268
	 * @return ContentSecurity
269
	 */
270
	public static function defaultUbiquityDebug(string $livereloadServer = '127.0.0.1:35729'): ContentSecurity {
271
		$csp = self::defaultUbiquity();
272
		$config = Startup::$config;
273
		if ($config['debug'] && \Ubiquity\debug\LiveReload::hasLiveReload()) {
0 ignored issues
show
Bug introduced by
The type Ubiquity\debug\LiveReload was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
274
			$csp->addPolicyDefault(CspDirectives::CONNECT_SRC, "ws://$livereloadServer");
275
		}
276
		return $csp;
277
	}
278
}
279