HTTPCacheControlMiddleware::setSharedMaxAge()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
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);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Control\HTTP::augmentState() has been deprecated: 4.2.0:5.0.0 Use HTTPCacheControlMiddleware instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

54
        /** @scrutinizer ignore-deprecated */ HTTP::augmentState($request, $response);

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.

Loading history...
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;
0 ignored issues
show
introduced by
The private property $defaultState is not used, and could be removed.
Loading history...
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 = [
0 ignored issues
show
introduced by
The private property $defaultVary is not used, and could be removed.
Loading history...
139
        "X-Forwarded-Protocol" => true,
140
    ];
141
142
    /**
143
     * Default forcing level
144
     *
145
     * @config
146
     * @var int
147
     */
148
    private static $defaultForcingLevel = 0;
0 ignored issues
show
introduced by
The private property $defaultForcingLevel is not used, and could be removed.
Loading history...
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 = [
0 ignored issues
show
introduced by
The private property $allowed_directives is not used, and could be removed.
Loading history...
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)));
0 ignored issues
show
Bug introduced by
It seems like preg_split('/\s*,\s*/', trim($vary)) can also be of type false; however, parameter $input of array_filter() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
                $vary = array_filter(/** @scrutinizer ignore-type */ preg_split("/\s*,\s*/", trim($vary)));
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp can also be of type string. However, the property $modificationDate is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $vary 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...
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