Completed
Push — 3 ( 1d0cff...2b4954 )
by Damian
13:47
created

HTTPCacheControl::removeDirective()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Class HTTPCacheControl
5
 *
6
 * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
7
 */
8
class HTTPCacheControl extends SS_Object {
9
10
	/**
11
	 * @var static
12
	 */
13
	private static $inst;
14
15
	/**
16
	 * Store for all the current directives and their values
17
	 * Starts with an implicit config for disabled caching
18
	 *
19
	 * @var array
20
	 */
21
	private $state = array();
22
23
	/**
24
	 * Forcing level of previous setting; higher number wins
25
	 * Combination of consts belo
26
	 *w
27
	 * @var int
28
	 */
29
	protected $forcingLevel = 0;
30
31
	/**
32
	 * Forcing level forced, optionally combined with one of the below.
33
	 */
34
	const LEVEL_FORCED = 10;
35
36
	/**
37
	 * Forcing level caching disabled. Overrides public/private.
38
	 */
39
	const LEVEL_DISABLED = 3;
40
41
	/**
42
	 * Forcing level private-cached. Overrides public.
43
	 */
44
	const LEVEL_PRIVATE = 2;
45
46
	/**
47
	 * Forcing level public cached. Lowest priority.
48
	 */
49
	const LEVEL_PUBLIC = 1;
50
51
	/**
52
	 * Forcing level caching enabled.
53
	 */
54
	const LEVEL_ENABLED = 0;
55
56
57
	/**
58
	 * A list of allowed cache directives for HTTPResponses
59
	 *
60
	 * This doesn't include any experimental directives,
61
	 * use the config system to add to these if you want to enable them
62
	 *
63
	 * @config
64
	 * @var array
65
	 */
66
	private static $allowed_directives = array(
67
		'public',
68
		'private',
69
		'no-cache',
70
		'max-age',
71
		's-maxage',
72
		'must-revalidate',
73
		'proxy-revalidate',
74
		'no-store',
75
		'no-transform',
76
	);
77
78
	public function __construct()
79
	{
80
		parent::__construct();
81
82
		// If we've not been provided an initial state, then grab HTTP.cache_contrpl from config
83
		if (!$this->state) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->state of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
84
			$this->setDirectivesFromArray(Config::inst()->get('HTTP', 'cache_control'));
85
		}
86
	}
87
88
	/**
89
	 * Instruct the cache to apply a change with a given level, optionally
90
	 * modifying it with a force flag to increase priority of this action.
91
	 *
92
	 * If the apply level was successful, the change is made and the internal level
93
	 * threshold is incremented.
94
	 *
95
	 * @param int $level Priority of the given change
96
	 * @param bool $force If usercode has requested this action is forced to a higher priority.
97
	 * Note: Even if $force is set to true, other higher-priority forced changes can still
98
	 * cause a change to be rejected if it is below the required threshold.
99
	 * @return bool True if the given change is accepted, and that the internal
100
	 * level threshold is updated (if necessary) to the new minimum level.
101
	 */
102
	protected function applyChangeLevel($level, $force)
103
	{
104
		$forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0);
105
		if ($forcingLevel < $this->forcingLevel) {
106
			return false;
107
		}
108
		$this->forcingLevel = $forcingLevel;
109
		return true;
110
	}
111
112
	/**
113
	 * Low level method for setting directives include any experimental or custom ones added via config
114
	 *
115
	 * @param string $directive
116
	 * @param string|bool $value
117
	 *
118
	 * @return $this
119
	 */
120
	public function setDirective($directive, $value = null)
121
	{
122
		// make sure the directive is in the list of allowed directives
123
		$allowedDirectives = $this->config()->get('allowed_directives');
124
		$directive = strtolower($directive);
125
		if (in_array($directive, $allowedDirectives)) {
126
			$this->state[$directive] = $value;
127
		} else {
128
			throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed');
129
		}
130
		return $this;
131
	}
132
133
	/**
134
	 * Low level method to set directives from an associative array
135
	 *
136
	 * @param array $directives
137
	 *
138
	 * @return $this
139
	 */
140
	public function setDirectivesFromArray($directives)
141
	{
142
		foreach ($directives as $directive => $value) {
143
			// null values mean remove
144
			if (is_null($value)) {
145
				$this->removeDirective($directive);
146
			} else {
147
				// for legacy reasons we accept the string literal "true" as a bool
148
				// a bool value of true means there is no explicit value for the directive
149
				if ($value && (is_bool($value) || strtolower($value) === 'true')) {
150
					$value = null;
151
				}
152
				$this->setDirective($directive, $value);
153
			}
154
		}
155
		return $this;
156
	}
157
158
	/**
159
	 * Low level method for removing directives
160
	 *
161
	 * @param string $directive
162
	 *
163
	 * @return $this
164
	 */
165
	public function removeDirective($directive)
166
	{
167
		unset($this->state[strtolower($directive)]);
168
		return $this;
169
	}
170
171
	/**
172
	 * Low level method to check if a directive is currently set
173
	 *
174
	 * @param string $directive
175
	 *
176
	 * @return bool
177
	 */
178
	public function hasDirective($directive)
179
	{
180
		return array_key_exists(strtolower($directive), $this->state);
181
	}
182
183
	/**
184
	 * Low level method to get the value of a directive
185
	 *
186
	 * Note that `null` value is acceptable for a directive
187
	 *
188
	 * @param string $directive
189
	 *
190
	 * @return string|false|null
191
	 */
192
	public function getDirective($directive)
193
	{
194
		if ($this->hasDirective($directive)) {
195
			return $this->state[strtolower($directive)];
196
		}
197
		return false;
198
	}
199
200
	/**
201
	 * The cache should not store anything about the client request or server response.
202
	 *
203
	 * Set the no-store directive (also removes max-age and s-maxage for consistency purposes)
204
	 *
205
	 * @param bool $noStore
206
	 *
207
	 * @return $this
208
	 */
209
	public function setNoStore($noStore = true)
210
	{
211
		if ($noStore) {
212
			$this->setDirective('no-store');
213
			$this->removeDirective('max-age');
214
			$this->removeDirective('s-maxage');
215
		} else {
216
			$this->removeDirective('no-store');
217
		}
218
		return $this;
219
	}
220
221
	/**
222
	 * Forces caches to submit the request to the origin server for validation before releasing a cached copy.
223
	 *
224
	 * @param bool $noCache
225
	 *
226
	 * @return $this
227
	 */
228
	public function setNoCache($noCache = true)
229
	{
230
		if ($noCache) {
231
			$this->setDirective('no-cache');
232
		} else {
233
			$this->removeDirective('no-cache');
234
		}
235
		return $this;
236
	}
237
238
	/**
239
	 * Specifies the maximum amount of time (seconds) a resource will be considered fresh.
240
	 * This directive is relative to the time of the request.
241
	 *
242
	 * @param int $age
243
	 *
244
	 * @return $this
245
	 */
246
	public function setMaxAge($age)
247
	{
248
		$this->setDirective('max-age', $age);
249
		return $this;
250
	}
251
252
	/**
253
	 * Overrides max-age or the Expires header, but it only applies to shared caches (e.g., proxies)
254
	 * and is ignored by a private cache.
255
	 *
256
	 * @param int $age
257
	 *
258
	 * @return $this
259
	 */
260
	public function setSharedMaxAge($age)
261
	{
262
		$this->setDirective('s-maxage', $age);
263
		return $this;
264
	}
265
266
	/**
267
	 * The cache must verify the status of the stale resources before using it and expired ones should not be used.
268
	 *
269
	 * @param bool $mustRevalidate
270
	 *
271
	 * @return $this
272
	 */
273
	public function setMustRevalidate($mustRevalidate = true)
274
	{
275
		if ($mustRevalidate) {
276
			$this->setDirective('must-revalidate');
277
		} else {
278
			$this->removeDirective('must-revalidate');
279
		}
280
		return $this;
281
	}
282
283
	/**
284
	 * Simple way to set cache control header to a cacheable state.
285
	 * Use this method over `publicCache()` if you are unsure about caching details.
286
	 *
287
	 * Removes `no-store` and `no-cache` directives; other directives will remain in place.
288
	 * Use alongside `setMaxAge()` to indicate caching.
289
	 *
290
	 * Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required.
291
	 * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
292
	 *
293
	 * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
294
	 * @param bool $force Force the cache to public even if its unforced private or public
295
	 * @return $this
296
	 */
297
	public function enableCache($force = false)
298
	{
299
		// Only execute this if its forcing level is high enough
300
		if (!$this->applyChangeLevel(self::LEVEL_ENABLED, $force)) {
301
			SS_Log::log("Call to enableCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
302
			return $this;
303
		}
304
305
		$this->removeDirective('no-store');
306
		$this->removeDirective('no-cache');
307
		return $this;
308
	}
309
310
	/**
311
	 * Simple way to set cache control header to a non-cacheable state.
312
	 * Use this method over `privateCache()` if you are unsure about caching details.
313
	 * Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
314
	 *
315
	 * Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
316
	 * the others are added under recommendation from Mozilla (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
317
	 *
318
	 * Does not set `private` directive, use `privateCache()` if this is explicitly required.
319
	 * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
320
	 *
321
	 * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
322
	 * @param bool $force Force the cache to diabled even if it's forced private or public
323
	 * @return $this
324
	 */
325
	public function disableCache($force = false)
326
	{
327
		// Only execute this if its forcing level is high enough
328
		if (!$this->applyChangeLevel(self::LEVEL_DISABLED, $force )) {
329
			SS_Log::log("Call to disableCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
330
			return $this;
331
		}
332
333
		$this->state = array(
334
			'no-cache' => null,
335
			'no-store' => null,
336
			'must-revalidate' => null,
337
		);
338
		return $this;
339
	}
340
341
	/**
342
	 * Advanced way to set cache control header to a non-cacheable state.
343
	 * Indicates that the response is intended for a single user and must not be stored by a shared cache.
344
	 * A private cache (e.g. Web Browser) may store the response. Also removes `public` as this is a contradictory directive.
345
	 *
346
	 * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
347
	 * @param bool $force Force the cache to private even if it's forced public
348
	 * @return $this
349
	 */
350
	public function privateCache($force = false)
351
	{
352
		// Only execute this if its forcing level is high enough
353
		if (!$this->applyChangeLevel(self::LEVEL_PRIVATE, $force)) {
354
			SS_Log::log("Call to privateCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
355
			return $this;
356
		}
357
358
		// Update the directives
359
		$this->setDirective('private');
360
		$this->removeDirective('public');
361
		$this->removeDirective('no-cache');
362
		$this->removeDirective('no-store');
363
		return $this;
364
	}
365
366
	/**
367
 	 * Advanced way to set cache control header to a cacheable state.
368
	 * Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
369
	 * Also removes `private` as this is a contradictory directive
370
	 *
371
	 * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
372
	 * @param bool $force Force the cache to public even if it's private, unless it's been forced private
373
	 * @return $this
374
	 */
375
	public function publicCache($force = false)
376
	{
377
		// Only execute this if its forcing level is high enough
378
		if (!$this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
379
			SS_Log::log("Call to publicCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
380
			return $this;
381
		}
382
383
		$this->setDirective('public');
384
		$this->removeDirective('private');
385
		$this->removeDirective('no-cache');
386
		$this->removeDirective('no-store');
387
		return $this;
388
	}
389
390
	/**
391
	 * Generate and add the `Cache-Control` header to a response object
392
	 *
393
	 * @param SS_HTTPResponse $response
394
	 *
395
	 * @return $this
396
	 */
397
	public function applyToResponse($response)
398
	{
399
		$headers = $this->generateHeaders();
400
		foreach ($headers as $name => $value) {
401
			$response->addHeader($name, $value);
402
		}
403
		return $this;
404
	}
405
406
	/**
407
	 * Generate the cache header
408
	 *
409
	 * @return string
410
	 */
411
	protected function generateCacheHeader()
412
	{
413
		$cacheControl = array();
414
		foreach ($this->state as $directive => $value) {
415
			if (is_null($value)) {
416
				$cacheControl[] = $directive;
417
			} else {
418
				$cacheControl[] = $directive . '=' . $value;
419
			}
420
		}
421
		return implode(', ', $cacheControl);
422
	}
423
424
	/**
425
	 * Generate all headers to output
426
	 *
427
	 * @return array
428
	 */
429
	public function generateHeaders()
430
	{
431
		return array(
432
			'Cache-Control' => $this->generateCacheHeader(),
433
		);
434
	}
435
436
	/**
437
	 * Reset registered http cache control and force a fresh instance to be built
438
	 */
439
	public static function reset() {
440
		Injector::inst()->unregisterNamedObject(__CLASS__);
441
	}
442
}
443