Completed
Push — 4 ( 5fbfd8...bd8494 )
by Ingo
09:20
created

HTTPCacheControlMiddleware   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 761
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 761
rs 1.8461
c 0
b 0
f 0
wmc 75

36 Methods

Rating   Name   Duplication   Size   Complexity  
B process() 0 24 4
B combineVary() 0 12 6
A addVary() 0 5 1
A getVary() 0 10 2
A setVary() 0 4 1
A setMustRevalidate() 0 5 1
A enableCache() 0 7 2
A setMaxAge() 0 6 1
A setStateDirectivesFromArray() 0 6 2
A getState() 0 3 2
A getDirectives() 0 3 1
A removeStateDirective() 0 4 1
A getDirective() 0 3 1
A setNoCache() 0 6 1
A getForcingLevel() 0 6 2
A generateCacheHeader() 0 11 3
A setNoStore() 0 12 2
A generateLastModifiedHeader() 0 6 2
A hasDirective() 0 3 1
A disableCache() 0 7 2
A hasStateDirective() 0 4 1
A generateHeadersFor() 0 7 1
A getStateDirectives() 0 3 1
A setSharedMaxAge() 0 6 1
A augmentState() 0 18 4
A getStateDirective() 0 7 2
A reset() 0 3 1
A applyToResponse() 0 9 3
B setStateDirective() 0 23 6
A publicCache() 0 7 2
A generateExpiresHeader() 0 10 2
A privateCache() 0 7 2
A setState() 0 7 2
A applyChangeLevel() 0 8 3
A generateVaryHeader() 0 12 3
A registerModificationDate() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like HTTPCacheControlMiddleware often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HTTPCacheControlMiddleware, and based on these observations, apply Extract Interface, too.

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..5.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
            'must-revalidate' => true,
92
        ],
93
    ];
94
95
    /**
96
     * Set default state
97
     *
98
     * @config
99
     * @var string
100
     */
101
    protected static $defaultState = self::STATE_DISABLED;
102
103
    /**
104
     * Current state
105
     *
106
     * @var string
107
     */
108
    protected $state = null;
109
110
    /**
111
     * Forcing level of previous setting; higher number wins
112
     * Combination of consts below
113
     *
114
     * @var int
115
     */
116
    protected $forcingLevel = null;
117
118
    /**
119
     * List of vary keys
120
     *
121
     * @var array|null
122
     */
123
    protected $vary = null;
124
125
    /**
126
     * Latest modification date for this response
127
     *
128
     * @var int
129
     */
130
    protected $modificationDate;
131
132
    /**
133
     * Default vary
134
     *
135
     * @var array
136
     */
137
    private static $defaultVary = [
0 ignored issues
show
introduced by
The private property $defaultVary is not used, and could be removed.
Loading history...
138
        "X-Requested-With" => true,
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
        $this->setStateDirective($applyTo, 'no-cache', $noCache);
497
        return $this;
498
    }
499
500
    /**
501
     * Specifies the maximum amount of time (seconds) a resource will be considered fresh.
502
     * This directive is relative to the time of the request.
503
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
504
     *
505
     * @param int $age
506
     * @return $this
507
     */
508
    public function setMaxAge($age)
509
    {
510
        // Affect all non-disabled states
511
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
512
        $this->setStateDirective($applyTo, 'max-age', $age);
513
        return $this;
514
    }
515
516
    /**
517
     * Overrides max-age or the Expires header, but it only applies to shared caches (e.g., proxies)
518
     * and is ignored by a private cache.
519
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
520
     *
521
     * @param int $age
522
     * @return $this
523
     */
524
    public function setSharedMaxAge($age)
525
    {
526
        // Affect all non-disabled states
527
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
528
        $this->setStateDirective($applyTo, 's-maxage', $age);
529
        return $this;
530
    }
531
532
    /**
533
     * The cache must verify the status of the stale resources before using it and expired ones should not be used.
534
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
535
     *
536
     * @param bool $mustRevalidate
537
     * @return $this
538
     */
539
    public function setMustRevalidate($mustRevalidate = true)
540
    {
541
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
542
        $this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate);
543
        return $this;
544
    }
545
546
    /**
547
     * Simple way to set cache control header to a cacheable state.
548
     *
549
     * The resulting cache-control headers will be chosen from the 'enabled' set of directives.
550
     *
551
     * Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required.
552
     * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
553
     *
554
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
555
     * @param bool $force Force the cache to public even if its unforced private or public
556
     * @return $this
557
     */
558
    public function enableCache($force = false)
559
    {
560
        // Only execute this if its forcing level is high enough
561
        if ($this->applyChangeLevel(self::LEVEL_ENABLED, $force)) {
562
            $this->setState(self::STATE_ENABLED);
563
        }
564
        return $this;
565
    }
566
567
    /**
568
     * Simple way to set cache control header to a non-cacheable state.
569
     * Use this method over `privateCache()` if you are unsure about caching details.
570
     * Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
571
     *
572
     * The resulting cache-control headers will be chosen from the 'disabled' set of directives.
573
     *
574
     * Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
575
     * the others are added under recommendation from Mozilla (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
576
     *
577
     * Does not set `private` directive, use `privateCache()` if this is explicitly required.
578
     * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
579
     *
580
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
581
     * @param bool $force Force the cache to diabled even if it's forced private or public
582
     * @return $this
583
     */
584
    public function disableCache($force = false)
585
    {
586
        // Only execute this if its forcing level is high enough
587
        if ($this->applyChangeLevel(self::LEVEL_DISABLED, $force)) {
588
            $this->setState(self::STATE_DISABLED);
589
        }
590
        return $this;
591
    }
592
593
    /**
594
     * Advanced way to set cache control header to a non-cacheable state.
595
     * Indicates that the response is intended for a single user and must not be stored by a shared cache.
596
     * A private cache (e.g. Web Browser) may store the response.
597
     *
598
     * The resulting cache-control headers will be chosen from the 'private' set of directives.
599
     *
600
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
601
     * @param bool $force Force the cache to private even if it's forced public
602
     * @return $this
603
     */
604
    public function privateCache($force = false)
605
    {
606
        // Only execute this if its forcing level is high enough
607
        if ($this->applyChangeLevel(self::LEVEL_PRIVATE, $force)) {
608
            $this->setState(self::STATE_PRIVATE);
609
        }
610
        return $this;
611
    }
612
613
    /**
614
     * Advanced way to set cache control header to a cacheable state.
615
     * Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
616
     *
617
     * The resulting cache-control headers will be chosen from the 'private' set of directives.
618
     *
619
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
620
     * @param bool $force Force the cache to public even if it's private, unless it's been forced private
621
     * @return $this
622
     */
623
    public function publicCache($force = false)
624
    {
625
        // Only execute this if its forcing level is high enough
626
        if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
627
            $this->setState(self::STATE_PUBLIC);
628
        }
629
        return $this;
630
    }
631
632
    /**
633
     * Generate all headers to add to this object
634
     *
635
     * @param HTTPResponse $response
636
     *
637
     * @return $this
638
     */
639
    public function applyToResponse($response)
640
    {
641
        $headers = $this->generateHeadersFor($response);
642
        foreach ($headers as $name => $value) {
643
            if (!$response->getHeader($name)) {
644
                $response->addHeader($name, $value);
645
            }
646
        }
647
        return $this;
648
    }
649
650
    /**
651
     * Generate the cache header
652
     *
653
     * @return string
654
     */
655
    protected function generateCacheHeader()
656
    {
657
        $cacheControl = [];
658
        foreach ($this->getDirectives() as $directive => $value) {
659
            if ($value === true) {
660
                $cacheControl[] = $directive;
661
            } else {
662
                $cacheControl[] = $directive . '=' . $value;
663
            }
664
        }
665
        return implode(', ', $cacheControl);
666
    }
667
668
    /**
669
     * Generate all headers to output
670
     *
671
     * @param HTTPResponse $response
672
     * @return array
673
     */
674
    public function generateHeadersFor(HTTPResponse $response)
675
    {
676
        return array_filter([
677
            'Last-Modified' => $this->generateLastModifiedHeader(),
678
            'Vary' => $this->generateVaryHeader($response),
679
            'Cache-Control' => $this->generateCacheHeader(),
680
            'Expires' => $this->generateExpiresHeader(),
681
        ]);
682
    }
683
684
    /**
685
     * Reset registered http cache control and force a fresh instance to be built
686
     */
687
    public static function reset()
688
    {
689
        Injector::inst()->unregisterNamedObject(__CLASS__);
690
    }
691
692
    /**
693
     * @return int
694
     */
695
    protected function getForcingLevel()
696
    {
697
        if (isset($this->forcingLevel)) {
698
            return $this->forcingLevel;
699
        }
700
        return $this->config()->get('defaultForcingLevel');
701
    }
702
703
    /**
704
     * Generate vary http header
705
     *
706
     * @param HTTPResponse $response
707
     * @return string|null
708
     */
709
    protected function generateVaryHeader(HTTPResponse $response)
710
    {
711
        // split the current vary header into it's parts and merge it with the config settings
712
        // to create a list of unique vary values
713
        $vary = $this->getVary();
714
        if ($response->getHeader('Vary')) {
715
            $vary = $this->combineVary($vary, $response->getHeader('Vary'));
716
        }
717
        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...
718
            return implode(', ', $vary);
719
        }
720
        return null;
721
    }
722
723
    /**
724
     * Generate Last-Modified header
725
     *
726
     * @return string|null
727
     */
728
    protected function generateLastModifiedHeader()
729
    {
730
        if (!$this->modificationDate) {
731
            return null;
732
        }
733
        return gmdate('D, d M Y H:i:s', $this->modificationDate) . ' GMT';
734
    }
735
736
    /**
737
     * Generate Expires http header
738
     *
739
     * @return null|string
740
     */
741
    protected function generateExpiresHeader()
742
    {
743
        $maxAge = $this->getDirective('max-age');
744
        if ($maxAge === false) {
745
            return null;
746
        }
747
748
        // Add now to max-age to generate expires
749
        $expires = DBDatetime::now()->getTimestamp() + $maxAge;
750
        return gmdate('D, d M Y H:i:s', $expires) . ' GMT';
751
    }
752
753
    /**
754
     * Update state based on current request and response objects
755
     *
756
     * @param HTTPRequest $request
757
     * @param HTTPResponse $response
758
     */
759
    protected function augmentState(HTTPRequest $request, HTTPResponse $response)
760
    {
761
        // If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are
762
        // likely to be supplying information relevant to the current user only
763
        if ($request->getSession()->getAll()) {
764
            // Don't force in case user code chooses to opt in to public caching
765
            $this->privateCache();
766
        }
767
768
        // Errors disable cache (unless some errors are cached intentionally by usercode)
769
        if ($response->isError()) {
770
            // Even if publicCache(true) is specified, errors will be uncacheable
771
            $this->disableCache(true);
772
        }
773
774
        // Don't cache redirects
775
        if ($response->isRedirect()) {
776
            $this->disableCache(true);
777
        }
778
    }
779
}
780