1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Control\Middleware; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use SilverStripe\Control\HTTP; |
7
|
|
|
use SilverStripe\Control\HTTPRequest; |
8
|
|
|
use SilverStripe\Control\HTTPResponse; |
9
|
|
|
use SilverStripe\Control\HTTPResponse_Exception; |
10
|
|
|
use SilverStripe\Core\Config\Configurable; |
11
|
|
|
use SilverStripe\Core\Injector\Injectable; |
12
|
|
|
use SilverStripe\Core\Injector\Injector; |
13
|
|
|
use SilverStripe\Core\Resettable; |
14
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime; |
15
|
|
|
|
16
|
|
|
class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable |
17
|
|
|
{ |
18
|
|
|
use Configurable; |
19
|
|
|
use Injectable; |
20
|
|
|
|
21
|
|
|
const STATE_ENABLED = 'enabled'; |
22
|
|
|
|
23
|
|
|
const STATE_PUBLIC = 'public'; |
24
|
|
|
|
25
|
|
|
const STATE_PRIVATE = 'private'; |
26
|
|
|
|
27
|
|
|
const STATE_DISABLED = 'disabled'; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Generate response for the given request |
31
|
|
|
* |
32
|
|
|
* @todo Refactor HTTP::add_cache_headers() (e.g. etag handling) into this middleware |
33
|
|
|
* |
34
|
|
|
* @param HTTPRequest $request |
35
|
|
|
* @param callable $delegate |
36
|
|
|
* @return HTTPResponse |
37
|
|
|
* @throws HTTPResponse_Exception |
38
|
|
|
*/ |
39
|
|
|
public function process(HTTPRequest $request, callable $delegate) |
40
|
|
|
{ |
41
|
|
|
try { |
42
|
|
|
$response = $delegate($request); |
43
|
|
|
} catch (HTTPResponse_Exception $ex) { |
44
|
|
|
$response = $ex->getResponse(); |
45
|
|
|
} |
46
|
|
|
if (!$response) { |
47
|
|
|
return null; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
// Update state based on current request and response objects |
51
|
|
|
$this->augmentState($request, $response); |
52
|
|
|
|
53
|
|
|
// Update state based on deprecated HTTP settings |
54
|
|
|
HTTP::augmentState($request, $response); |
|
|
|
|
55
|
|
|
|
56
|
|
|
// Add all headers to this response object |
57
|
|
|
$this->applyToResponse($response); |
58
|
|
|
|
59
|
|
|
if (isset($ex)) { |
60
|
|
|
throw $ex; |
61
|
|
|
} |
62
|
|
|
return $response; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* List of states, each of which contains a key of standard directives. |
67
|
|
|
* Each directive should either be a numeric value, true to enable, |
68
|
|
|
* or (bool)false or null to disable. |
69
|
|
|
* Top level key states include `disabled`, `private`, `public`, `enabled` |
70
|
|
|
* in descending order of precedence. |
71
|
|
|
* |
72
|
|
|
* This allows directives to be set independently for individual states. |
73
|
|
|
* |
74
|
|
|
* @var array |
75
|
|
|
*/ |
76
|
|
|
protected $stateDirectives = [ |
77
|
|
|
self::STATE_DISABLED => [ |
78
|
|
|
'no-cache' => true, |
79
|
|
|
'no-store' => true, |
80
|
|
|
'must-revalidate' => true, |
81
|
|
|
], |
82
|
|
|
self::STATE_PRIVATE => [ |
83
|
|
|
'private' => true, |
84
|
|
|
'must-revalidate' => true, |
85
|
|
|
], |
86
|
|
|
self::STATE_PUBLIC => [ |
87
|
|
|
'public' => true, |
88
|
|
|
'must-revalidate' => true, |
89
|
|
|
], |
90
|
|
|
self::STATE_ENABLED => [ |
91
|
|
|
'no-cache' => true, |
92
|
|
|
'must-revalidate' => true, |
93
|
|
|
] |
94
|
|
|
]; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Set default state |
98
|
|
|
* |
99
|
|
|
* @config |
100
|
|
|
* @var string |
101
|
|
|
*/ |
102
|
|
|
private static $defaultState = self::STATE_ENABLED; |
|
|
|
|
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Current state |
106
|
|
|
* |
107
|
|
|
* @var string |
108
|
|
|
*/ |
109
|
|
|
protected $state = null; |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Forcing level of previous setting; higher number wins |
113
|
|
|
* Combination of consts below |
114
|
|
|
* |
115
|
|
|
* @var int |
116
|
|
|
*/ |
117
|
|
|
protected $forcingLevel = null; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* List of vary keys |
121
|
|
|
* |
122
|
|
|
* @var array|null |
123
|
|
|
*/ |
124
|
|
|
protected $vary = null; |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Latest modification date for this response |
128
|
|
|
* |
129
|
|
|
* @var int |
130
|
|
|
*/ |
131
|
|
|
protected $modificationDate; |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Default vary |
135
|
|
|
* |
136
|
|
|
* @var array |
137
|
|
|
*/ |
138
|
|
|
private static $defaultVary = [ |
|
|
|
|
139
|
|
|
"X-Forwarded-Protocol" => true, |
140
|
|
|
]; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Default forcing level |
144
|
|
|
* |
145
|
|
|
* @config |
146
|
|
|
* @var int |
147
|
|
|
*/ |
148
|
|
|
private static $defaultForcingLevel = 0; |
|
|
|
|
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Forcing level forced, optionally combined with one of the below. |
152
|
|
|
*/ |
153
|
|
|
const LEVEL_FORCED = 10; |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Forcing level caching disabled. Overrides public/private. |
157
|
|
|
*/ |
158
|
|
|
const LEVEL_DISABLED = 3; |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Forcing level private-cached. Overrides public. |
162
|
|
|
*/ |
163
|
|
|
const LEVEL_PRIVATE = 2; |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Forcing level public cached. Lowest priority. |
167
|
|
|
*/ |
168
|
|
|
const LEVEL_PUBLIC = 1; |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Forcing level caching enabled. |
172
|
|
|
*/ |
173
|
|
|
const LEVEL_ENABLED = 0; |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* A list of allowed cache directives for HTTPResponses |
177
|
|
|
* |
178
|
|
|
* This doesn't include any experimental directives, |
179
|
|
|
* use the config system to add to these if you want to enable them |
180
|
|
|
* |
181
|
|
|
* @config |
182
|
|
|
* @var array |
183
|
|
|
*/ |
184
|
|
|
private static $allowed_directives = [ |
|
|
|
|
185
|
|
|
'public', |
186
|
|
|
'private', |
187
|
|
|
'no-cache', |
188
|
|
|
'max-age', |
189
|
|
|
's-maxage', |
190
|
|
|
'must-revalidate', |
191
|
|
|
'proxy-revalidate', |
192
|
|
|
'no-store', |
193
|
|
|
'no-transform', |
194
|
|
|
]; |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Get current vary keys |
198
|
|
|
* |
199
|
|
|
* @return array |
200
|
|
|
*/ |
201
|
|
|
public function getVary() |
202
|
|
|
{ |
203
|
|
|
// Explicitly set vary |
204
|
|
|
if (isset($this->vary)) { |
205
|
|
|
return $this->vary; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
// Load default from config |
209
|
|
|
$defaultVary = $this->config()->get('defaultVary'); |
210
|
|
|
return array_keys(array_filter($defaultVary)); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Add a vary |
215
|
|
|
* |
216
|
|
|
* @param string|array $vary |
217
|
|
|
* @return $this |
218
|
|
|
*/ |
219
|
|
|
public function addVary($vary) |
220
|
|
|
{ |
221
|
|
|
$combied = $this->combineVary($this->getVary(), $vary); |
222
|
|
|
$this->setVary($combied); |
223
|
|
|
return $this; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Set vary |
228
|
|
|
* |
229
|
|
|
* @param array|string $vary |
230
|
|
|
* @return $this |
231
|
|
|
*/ |
232
|
|
|
public function setVary($vary) |
233
|
|
|
{ |
234
|
|
|
$this->vary = $this->combineVary($vary); |
235
|
|
|
return $this; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Combine vary strings/arrays into a single array, or normalise a single vary |
240
|
|
|
* |
241
|
|
|
* @param string|array[] $varies Each vary as a separate arg |
242
|
|
|
* @return array |
243
|
|
|
*/ |
244
|
|
|
protected function combineVary(...$varies) |
245
|
|
|
{ |
246
|
|
|
$merged = []; |
247
|
|
|
foreach ($varies as $vary) { |
248
|
|
|
if ($vary && is_string($vary)) { |
249
|
|
|
$vary = array_filter(preg_split("/\s*,\s*/", trim($vary))); |
|
|
|
|
250
|
|
|
} |
251
|
|
|
if ($vary && is_array($vary)) { |
252
|
|
|
$merged = array_merge($merged, $vary); |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
return array_unique($merged); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Register a modification date. Used to calculate the "Last-Modified" HTTP header. |
261
|
|
|
* Can be called multiple times, and will automatically retain the most recent date. |
262
|
|
|
* |
263
|
|
|
* @param string|int $date Date string or timestamp |
264
|
|
|
* @return HTTPCacheControlMiddleware |
265
|
|
|
*/ |
266
|
|
|
public function registerModificationDate($date) |
267
|
|
|
{ |
268
|
|
|
$timestamp = is_numeric($date) ? $date : strtotime($date); |
269
|
|
|
if ($timestamp > $this->modificationDate) { |
270
|
|
|
$this->modificationDate = $timestamp; |
|
|
|
|
271
|
|
|
} |
272
|
|
|
return $this; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Set current state. Should only be invoked internally after processing precedence rules. |
277
|
|
|
* |
278
|
|
|
* @param string $state |
279
|
|
|
* @return $this |
280
|
|
|
*/ |
281
|
|
|
protected function setState($state) |
282
|
|
|
{ |
283
|
|
|
if (!array_key_exists($state, $this->stateDirectives)) { |
284
|
|
|
throw new InvalidArgumentException("Invalid state {$state}"); |
285
|
|
|
} |
286
|
|
|
$this->state = $state; |
287
|
|
|
return $this; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Get current state |
292
|
|
|
* |
293
|
|
|
* @return string |
294
|
|
|
*/ |
295
|
|
|
public function getState() |
296
|
|
|
{ |
297
|
|
|
return $this->state ?: $this->config()->get('defaultState'); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Instruct the cache to apply a change with a given level, optionally |
302
|
|
|
* modifying it with a force flag to increase priority of this action. |
303
|
|
|
* |
304
|
|
|
* If the apply level was successful, the change is made and the internal level |
305
|
|
|
* threshold is incremented. |
306
|
|
|
* |
307
|
|
|
* @param int $level Priority of the given change |
308
|
|
|
* @param bool $force If usercode has requested this action is forced to a higher priority. |
309
|
|
|
* Note: Even if $force is set to true, other higher-priority forced changes can still |
310
|
|
|
* cause a change to be rejected if it is below the required threshold. |
311
|
|
|
* @return bool True if the given change is accepted, and that the internal |
312
|
|
|
* level threshold is updated (if necessary) to the new minimum level. |
313
|
|
|
*/ |
314
|
|
|
protected function applyChangeLevel($level, $force) |
315
|
|
|
{ |
316
|
|
|
$forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0); |
317
|
|
|
if ($forcingLevel < $this->getForcingLevel()) { |
318
|
|
|
return false; |
319
|
|
|
} |
320
|
|
|
$this->forcingLevel = $forcingLevel; |
321
|
|
|
return true; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Low level method for setting directives include any experimental or custom ones added via config. |
326
|
|
|
* You need to specify the state (or states) to apply this directive to. |
327
|
|
|
* Can also remove directives with false |
328
|
|
|
* |
329
|
|
|
* @param array|string $states State(s) to apply this directive to |
330
|
|
|
* @param string $directive |
331
|
|
|
* @param int|string|bool $value Flag to set for this value. Set to false to remove, or true to set. |
332
|
|
|
* String or int value assign a specific value. |
333
|
|
|
* @return $this |
334
|
|
|
*/ |
335
|
|
|
public function setStateDirective($states, $directive, $value = true) |
336
|
|
|
{ |
337
|
|
|
if ($value === null) { |
|
|
|
|
338
|
|
|
throw new InvalidArgumentException("Invalid directive value"); |
339
|
|
|
} |
340
|
|
|
// make sure the directive is in the list of allowed directives |
341
|
|
|
$allowedDirectives = $this->config()->get('allowed_directives'); |
342
|
|
|
$directive = strtolower($directive); |
343
|
|
|
if (!in_array($directive, $allowedDirectives)) { |
344
|
|
|
throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed'); |
345
|
|
|
} |
346
|
|
|
foreach ((array)$states as $state) { |
347
|
|
|
if (!array_key_exists($state, $this->stateDirectives)) { |
348
|
|
|
throw new InvalidArgumentException("Invalid state {$state}"); |
349
|
|
|
} |
350
|
|
|
// Set or unset directive |
351
|
|
|
if ($value === false) { |
352
|
|
|
unset($this->stateDirectives[$state][$directive]); |
353
|
|
|
} else { |
354
|
|
|
$this->stateDirectives[$state][$directive] = $value; |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
return $this; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Low level method to set directives from an associative array |
362
|
|
|
* |
363
|
|
|
* @param array|string $states State(s) to apply this directive to |
364
|
|
|
* @param array $directives |
365
|
|
|
* @return $this |
366
|
|
|
*/ |
367
|
|
|
public function setStateDirectivesFromArray($states, $directives) |
368
|
|
|
{ |
369
|
|
|
foreach ($directives as $directive => $value) { |
370
|
|
|
$this->setStateDirective($states, $directive, $value); |
371
|
|
|
} |
372
|
|
|
return $this; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Low level method for removing directives |
377
|
|
|
* |
378
|
|
|
* @param array|string $states State(s) to remove this directive from |
379
|
|
|
* @param string $directive |
380
|
|
|
* @return $this |
381
|
|
|
*/ |
382
|
|
|
public function removeStateDirective($states, $directive) |
383
|
|
|
{ |
384
|
|
|
$this->setStateDirective($states, $directive, false); |
385
|
|
|
return $this; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Low level method to check if a directive is currently set |
390
|
|
|
* |
391
|
|
|
* @param string $state State(s) to apply this directive to |
392
|
|
|
* @param string $directive |
393
|
|
|
* @return bool |
394
|
|
|
*/ |
395
|
|
|
public function hasStateDirective($state, $directive) |
396
|
|
|
{ |
397
|
|
|
$directive = strtolower($directive); |
398
|
|
|
return isset($this->stateDirectives[$state][$directive]); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Check if the current state has the given directive. |
403
|
|
|
* |
404
|
|
|
* @param string $directive |
405
|
|
|
* @return bool |
406
|
|
|
*/ |
407
|
|
|
public function hasDirective($directive) |
408
|
|
|
{ |
409
|
|
|
return $this->hasStateDirective($this->getState(), $directive); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* Low level method to get the value of a directive for a state. |
414
|
|
|
* Returns false if there is no directive. |
415
|
|
|
* True means the flag is set, otherwise the value of the directive. |
416
|
|
|
* |
417
|
|
|
* @param string $state |
418
|
|
|
* @param string $directive |
419
|
|
|
* @return int|string|bool |
420
|
|
|
*/ |
421
|
|
|
public function getStateDirective($state, $directive) |
422
|
|
|
{ |
423
|
|
|
$directive = strtolower($directive); |
424
|
|
|
if (isset($this->stateDirectives[$state][$directive])) { |
425
|
|
|
return $this->stateDirectives[$state][$directive]; |
426
|
|
|
} |
427
|
|
|
return false; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Get the value of the given directive for the current state |
432
|
|
|
* |
433
|
|
|
* @param string $directive |
434
|
|
|
* @return bool|int|string |
435
|
|
|
*/ |
436
|
|
|
public function getDirective($directive) |
437
|
|
|
{ |
438
|
|
|
return $this->getStateDirective($this->getState(), $directive); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Get directives for the given state |
443
|
|
|
* |
444
|
|
|
* @param string $state |
445
|
|
|
* @return array |
446
|
|
|
*/ |
447
|
|
|
public function getStateDirectives($state) |
448
|
|
|
{ |
449
|
|
|
return $this->stateDirectives[$state]; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* Get all directives for the currently active state |
454
|
|
|
* |
455
|
|
|
* @return array |
456
|
|
|
*/ |
457
|
|
|
public function getDirectives() |
458
|
|
|
{ |
459
|
|
|
return $this->getStateDirectives($this->getState()); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* The cache should not store anything about the client request or server response. |
464
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state. |
465
|
|
|
* Set the no-store directive (also removes max-age and s-maxage for consistency purposes) |
466
|
|
|
* |
467
|
|
|
* @param bool $noStore |
468
|
|
|
* |
469
|
|
|
* @return $this |
470
|
|
|
*/ |
471
|
|
|
public function setNoStore($noStore = true) |
472
|
|
|
{ |
473
|
|
|
// Affect all non-disabled states |
474
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; |
475
|
|
|
if ($noStore) { |
476
|
|
|
$this->setStateDirective($applyTo, 'no-store'); |
477
|
|
|
$this->removeStateDirective($applyTo, 'max-age'); |
478
|
|
|
$this->removeStateDirective($applyTo, 's-maxage'); |
479
|
|
|
} else { |
480
|
|
|
$this->removeStateDirective($applyTo, 'no-store'); |
481
|
|
|
} |
482
|
|
|
return $this; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* Forces caches to submit the request to the origin server for validation before releasing a cached copy. |
487
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state. |
488
|
|
|
* |
489
|
|
|
* @param bool $noCache |
490
|
|
|
* @return $this |
491
|
|
|
*/ |
492
|
|
|
public function setNoCache($noCache = true) |
493
|
|
|
{ |
494
|
|
|
// Affect all non-disabled states |
495
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; |
496
|
|
|
if ($noCache) { |
497
|
|
|
$this->setStateDirective($applyTo, 'no-cache'); |
498
|
|
|
$this->removeStateDirective($applyTo, 'max-age'); |
499
|
|
|
$this->removeStateDirective($applyTo, 's-maxage'); |
500
|
|
|
} else { |
501
|
|
|
$this->removeStateDirective($applyTo, 'no-cache'); |
502
|
|
|
} |
503
|
|
|
return $this; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
/** |
507
|
|
|
* Specifies the maximum amount of time (seconds) a resource will be considered fresh. |
508
|
|
|
* This directive is relative to the time of the request. |
509
|
|
|
* Affects all non-disabled states. Use enableCache(), publicCache() or |
510
|
|
|
* setStateDirective() instead to set the max age for a single state. |
511
|
|
|
* |
512
|
|
|
* @param int $age |
513
|
|
|
* @return $this |
514
|
|
|
*/ |
515
|
|
|
public function setMaxAge($age) |
516
|
|
|
{ |
517
|
|
|
// Affect all non-disabled states |
518
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; |
519
|
|
|
$this->setStateDirective($applyTo, 'max-age', $age); |
520
|
|
|
if ($age) { |
521
|
|
|
$this->removeStateDirective($applyTo, 'no-cache'); |
522
|
|
|
$this->removeStateDirective($applyTo, 'no-store'); |
523
|
|
|
} |
524
|
|
|
return $this; |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* Overrides max-age or the Expires header, but it only applies to shared caches (e.g., proxies) |
529
|
|
|
* and is ignored by a private cache. |
530
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state. |
531
|
|
|
* |
532
|
|
|
* @param int $age |
533
|
|
|
* @return $this |
534
|
|
|
*/ |
535
|
|
|
public function setSharedMaxAge($age) |
536
|
|
|
{ |
537
|
|
|
// Affect all non-disabled states |
538
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; |
539
|
|
|
$this->setStateDirective($applyTo, 's-maxage', $age); |
540
|
|
|
if ($age) { |
541
|
|
|
$this->removeStateDirective($applyTo, 'no-cache'); |
542
|
|
|
$this->removeStateDirective($applyTo, 'no-store'); |
543
|
|
|
} |
544
|
|
|
return $this; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* The cache must verify the status of the stale resources before using it and expired ones should not be used. |
549
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state. |
550
|
|
|
* |
551
|
|
|
* @param bool $mustRevalidate |
552
|
|
|
* @return $this |
553
|
|
|
*/ |
554
|
|
|
public function setMustRevalidate($mustRevalidate = true) |
555
|
|
|
{ |
556
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; |
557
|
|
|
$this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate); |
558
|
|
|
return $this; |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* Simple way to set cache control header to a cacheable state. |
563
|
|
|
* Needs either `setMaxAge()` or the `$maxAge` method argument in order to activate caching. |
564
|
|
|
* |
565
|
|
|
* The resulting cache-control headers will be chosen from the 'enabled' set of directives. |
566
|
|
|
* |
567
|
|
|
* Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is |
568
|
|
|
* explicitly required. |
569
|
|
|
* See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency... |
570
|
|
|
* /http-caching#public_vs_private |
571
|
|
|
* |
572
|
|
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ |
573
|
|
|
* @param bool $force Force the cache to public even if its unforced private or public |
574
|
|
|
* @param int $maxAge Shortcut for `setMaxAge()`, which is required to actually enable the cache. |
575
|
|
|
* @return $this |
576
|
|
|
*/ |
577
|
|
|
public function enableCache($force = false, $maxAge = null) |
578
|
|
|
{ |
579
|
|
|
// Only execute this if its forcing level is high enough |
580
|
|
|
if ($this->applyChangeLevel(self::LEVEL_ENABLED, $force)) { |
581
|
|
|
$this->setState(self::STATE_ENABLED); |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
if (!is_null($maxAge)) { |
585
|
|
|
$this->setMaxAge($maxAge); |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
return $this; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* Simple way to set cache control header to a non-cacheable state. |
593
|
|
|
* Use this method over `privateCache()` if you are unsure about caching details. |
594
|
|
|
* Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls. |
595
|
|
|
* |
596
|
|
|
* The resulting cache-control headers will be chosen from the 'disabled' set of directives. |
597
|
|
|
* |
598
|
|
|
* Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient |
599
|
|
|
* the others are added under recommendation from Mozilla |
600
|
|
|
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples) |
601
|
|
|
* |
602
|
|
|
* Does not set `private` directive, use `privateCache()` if this is explicitly required. |
603
|
|
|
* See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency... |
604
|
|
|
* /http-caching#public_vs_private |
605
|
|
|
* |
606
|
|
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ |
607
|
|
|
* @param bool $force Force the cache to diabled even if it's forced private or public |
608
|
|
|
* @return $this |
609
|
|
|
*/ |
610
|
|
|
public function disableCache($force = false) |
611
|
|
|
{ |
612
|
|
|
// Only execute this if its forcing level is high enough |
613
|
|
|
if ($this->applyChangeLevel(self::LEVEL_DISABLED, $force)) { |
614
|
|
|
$this->setState(self::STATE_DISABLED); |
615
|
|
|
} |
616
|
|
|
return $this; |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* Advanced way to set cache control header to a non-cacheable state. |
621
|
|
|
* Indicates that the response is intended for a single user and must not be stored by a shared cache. |
622
|
|
|
* A private cache (e.g. Web Browser) may store the response. |
623
|
|
|
* |
624
|
|
|
* The resulting cache-control headers will be chosen from the 'private' set of directives. |
625
|
|
|
* |
626
|
|
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ |
627
|
|
|
* @param bool $force Force the cache to private even if it's forced public |
628
|
|
|
* @return $this |
629
|
|
|
*/ |
630
|
|
|
public function privateCache($force = false) |
631
|
|
|
{ |
632
|
|
|
// Only execute this if its forcing level is high enough |
633
|
|
|
if ($this->applyChangeLevel(self::LEVEL_PRIVATE, $force)) { |
634
|
|
|
$this->setState(self::STATE_PRIVATE); |
635
|
|
|
} |
636
|
|
|
return $this; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
/** |
640
|
|
|
* Advanced way to set cache control header to a cacheable state. |
641
|
|
|
* Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers). |
642
|
|
|
* Needs either `setMaxAge()` or the `$maxAge` method argument in order to activate caching. |
643
|
|
|
* |
644
|
|
|
* The resulting cache-control headers will be chosen from the 'private' set of directives. |
645
|
|
|
* |
646
|
|
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ |
647
|
|
|
* @param bool $force Force the cache to public even if it's private, unless it's been forced private |
648
|
|
|
* @param int $maxAge Shortcut for `setMaxAge()`, which is required to actually enable the cache. |
649
|
|
|
* @return $this |
650
|
|
|
*/ |
651
|
|
|
public function publicCache($force = false, $maxAge = null) |
652
|
|
|
{ |
653
|
|
|
// Only execute this if its forcing level is high enough |
654
|
|
|
if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) { |
655
|
|
|
$this->setState(self::STATE_PUBLIC); |
656
|
|
|
} |
657
|
|
|
|
658
|
|
|
if (!is_null($maxAge)) { |
659
|
|
|
$this->setMaxAge($maxAge); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
return $this; |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Generate all headers to add to this object |
667
|
|
|
* |
668
|
|
|
* @param HTTPResponse $response |
669
|
|
|
* |
670
|
|
|
* @return $this |
671
|
|
|
*/ |
672
|
|
|
public function applyToResponse($response) |
673
|
|
|
{ |
674
|
|
|
$headers = $this->generateHeadersFor($response); |
675
|
|
|
foreach ($headers as $name => $value) { |
676
|
|
|
if (!$response->getHeader($name)) { |
677
|
|
|
$response->addHeader($name, $value); |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
return $this; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
/** |
684
|
|
|
* Generate the cache header |
685
|
|
|
* |
686
|
|
|
* @return string |
687
|
|
|
*/ |
688
|
|
|
protected function generateCacheHeader() |
689
|
|
|
{ |
690
|
|
|
$cacheControl = []; |
691
|
|
|
foreach ($this->getDirectives() as $directive => $value) { |
692
|
|
|
if ($value === true) { |
693
|
|
|
$cacheControl[] = $directive; |
694
|
|
|
} else { |
695
|
|
|
$cacheControl[] = $directive . '=' . $value; |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
return implode(', ', $cacheControl); |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
/** |
702
|
|
|
* Generate all headers to output |
703
|
|
|
* |
704
|
|
|
* @param HTTPResponse $response |
705
|
|
|
* @return array |
706
|
|
|
*/ |
707
|
|
|
public function generateHeadersFor(HTTPResponse $response) |
708
|
|
|
{ |
709
|
|
|
return array_filter([ |
710
|
|
|
'Last-Modified' => $this->generateLastModifiedHeader(), |
711
|
|
|
'Vary' => $this->generateVaryHeader($response), |
712
|
|
|
'Cache-Control' => $this->generateCacheHeader(), |
713
|
|
|
'Expires' => $this->generateExpiresHeader(), |
714
|
|
|
]); |
715
|
|
|
} |
716
|
|
|
|
717
|
|
|
/** |
718
|
|
|
* Reset registered http cache control and force a fresh instance to be built |
719
|
|
|
*/ |
720
|
|
|
public static function reset() |
721
|
|
|
{ |
722
|
|
|
Injector::inst()->unregisterNamedObject(__CLASS__); |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
/** |
726
|
|
|
* @return int |
727
|
|
|
*/ |
728
|
|
|
protected function getForcingLevel() |
729
|
|
|
{ |
730
|
|
|
if (isset($this->forcingLevel)) { |
731
|
|
|
return $this->forcingLevel; |
732
|
|
|
} |
733
|
|
|
return $this->config()->get('defaultForcingLevel'); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* Generate vary http header |
738
|
|
|
* |
739
|
|
|
* @param HTTPResponse $response |
740
|
|
|
* @return string|null |
741
|
|
|
*/ |
742
|
|
|
protected function generateVaryHeader(HTTPResponse $response) |
743
|
|
|
{ |
744
|
|
|
// split the current vary header into it's parts and merge it with the config settings |
745
|
|
|
// to create a list of unique vary values |
746
|
|
|
$vary = $this->getVary(); |
747
|
|
|
if ($response->getHeader('Vary')) { |
748
|
|
|
$vary = $this->combineVary($vary, $response->getHeader('Vary')); |
749
|
|
|
} |
750
|
|
|
if ($vary) { |
|
|
|
|
751
|
|
|
return implode(', ', $vary); |
752
|
|
|
} |
753
|
|
|
return null; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
/** |
757
|
|
|
* Generate Last-Modified header |
758
|
|
|
* |
759
|
|
|
* @return string|null |
760
|
|
|
*/ |
761
|
|
|
protected function generateLastModifiedHeader() |
762
|
|
|
{ |
763
|
|
|
if (!$this->modificationDate) { |
764
|
|
|
return null; |
765
|
|
|
} |
766
|
|
|
return gmdate('D, d M Y H:i:s', $this->modificationDate) . ' GMT'; |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Generate Expires http header |
771
|
|
|
* |
772
|
|
|
* @return null|string |
773
|
|
|
*/ |
774
|
|
|
protected function generateExpiresHeader() |
775
|
|
|
{ |
776
|
|
|
$maxAge = $this->getDirective('max-age'); |
777
|
|
|
if ($maxAge === false) { |
778
|
|
|
return null; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
// Add now to max-age to generate expires |
782
|
|
|
$expires = DBDatetime::now()->getTimestamp() + $maxAge; |
783
|
|
|
return gmdate('D, d M Y H:i:s', $expires) . ' GMT'; |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
/** |
787
|
|
|
* Update state based on current request and response objects |
788
|
|
|
* |
789
|
|
|
* @param HTTPRequest $request |
790
|
|
|
* @param HTTPResponse $response |
791
|
|
|
*/ |
792
|
|
|
protected function augmentState(HTTPRequest $request, HTTPResponse $response) |
793
|
|
|
{ |
794
|
|
|
// Errors disable cache (unless some errors are cached intentionally by usercode) |
795
|
|
|
if ($response->isError() || $response->isRedirect()) { |
796
|
|
|
// Even if publicCache(true) is specified, errors will be uncacheable |
797
|
|
|
$this->disableCache(true); |
798
|
|
|
} elseif ($request->getSession()->getAll()) { |
799
|
|
|
// If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are |
800
|
|
|
// likely to be supplying information relevant to the current user only |
801
|
|
|
|
802
|
|
|
// Don't force in case user code chooses to opt in to public caching |
803
|
|
|
$this->privateCache(); |
804
|
|
|
} |
805
|
|
|
} |
806
|
|
|
} |
807
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.