Passed
Push — master ( 16e851...25995c )
by Daniel
09:34
created

HTTPCacheControlMiddleware::getStateDirective()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 7
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
    const STATE_DEFAULT = 'default';
30
31
    /**
32
     * Generate response for the given request
33
     *
34
     * @todo Refactor HTTP::add_cache_headers() (e.g. etag handling) into this middleware
35
     *
36
     * @param HTTPRequest $request
37
     * @param callable $delegate
38
     * @return HTTPResponse
39
     * @throws HTTPResponse_Exception
40
     */
41
    public function process(HTTPRequest $request, callable $delegate)
42
    {
43
        try {
44
            $response = $delegate($request);
45
        } catch (HTTPResponse_Exception $ex) {
46
            $response = $ex->getResponse();
47
        }
48
        if (!$response) {
49
            return null;
50
        }
51
52
        // Update state based on current request and response objects
53
        $this->augmentState($request, $response);
54
55
        // Update state based on deprecated HTTP settings
56
        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

56
        /** @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...
57
58
        // Add all headers to this response object
59
        $this->applyToResponse($response);
60
61
        if (isset($ex)) {
62
            throw $ex;
63
        }
64
        return $response;
65
    }
66
67
    /**
68
     * List of states, each of which contains a key of standard directives.
69
     * Each directive should either be a numeric value, true to enable,
70
     * or (bool)false or null to disable.
71
     * Top level key states include `disabled`, `private`, `public`, `enabled`
72
     * in descending order of precedence.
73
     *
74
     * This allows directives to be set independently for individual states.
75
     *
76
     * @var array
77
     */
78
    protected $stateDirectives = [
79
        self::STATE_DISABLED => [
80
            'no-cache' => true,
81
            'no-store' => true,
82
            'must-revalidate' => true,
83
        ],
84
        self::STATE_PRIVATE => [
85
            'private' => true,
86
            'must-revalidate' => true,
87
        ],
88
        self::STATE_PUBLIC => [
89
            'public' => true,
90
            'must-revalidate' => true,
91
        ],
92
        self::STATE_ENABLED => [
93
            'must-revalidate' => true,
94
        ],
95
        self::STATE_DEFAULT => [
96
            'no-cache' => true,
97
        ],
98
    ];
99
100
    /**
101
     * Set default state
102
     *
103
     * @config
104
     * @var string
105
     */
106
    private static $defaultState = self::STATE_DEFAULT;
0 ignored issues
show
introduced by
The private property $defaultState is not used, and could be removed.
Loading history...
107
108
    /**
109
     * Current state
110
     *
111
     * @var string
112
     */
113
    protected $state = null;
114
115
    /**
116
     * Forcing level of previous setting; higher number wins
117
     * Combination of consts below
118
     *
119
     * @var int
120
     */
121
    protected $forcingLevel = null;
122
123
    /**
124
     * List of vary keys
125
     *
126
     * @var array|null
127
     */
128
    protected $vary = null;
129
130
    /**
131
     * Latest modification date for this response
132
     *
133
     * @var int
134
     */
135
    protected $modificationDate;
136
137
    /**
138
     * Default vary
139
     *
140
     * @var array
141
     */
142
    private static $defaultVary = [
0 ignored issues
show
introduced by
The private property $defaultVary is not used, and could be removed.
Loading history...
143
        "X-Requested-With" => true,
144
        "X-Forwarded-Protocol" => true,
145
    ];
146
147
    /**
148
     * Default forcing level
149
     *
150
     * @config
151
     * @var int
152
     */
153
    private static $defaultForcingLevel = 0;
0 ignored issues
show
introduced by
The private property $defaultForcingLevel is not used, and could be removed.
Loading history...
154
155
    /**
156
     * Forcing level forced, optionally combined with one of the below.
157
     */
158
    const LEVEL_FORCED = 10;
159
160
    /**
161
     * Forcing level caching disabled. Overrides public/private.
162
     */
163
    const LEVEL_DISABLED = 3;
164
165
    /**
166
     * Forcing level private-cached. Overrides public.
167
     */
168
    const LEVEL_PRIVATE = 2;
169
170
    /**
171
     * Forcing level public cached. Lowest priority.
172
     */
173
    const LEVEL_PUBLIC = 1;
174
175
    /**
176
     * Forcing level caching enabled.
177
     */
178
    const LEVEL_ENABLED = 0;
179
180
    /**
181
     * A list of allowed cache directives for HTTPResponses
182
     *
183
     * This doesn't include any experimental directives,
184
     * use the config system to add to these if you want to enable them
185
     *
186
     * @config
187
     * @var array
188
     */
189
    private static $allowed_directives = [
0 ignored issues
show
introduced by
The private property $allowed_directives is not used, and could be removed.
Loading history...
190
        'public',
191
        'private',
192
        'no-cache',
193
        'max-age',
194
        's-maxage',
195
        'must-revalidate',
196
        'proxy-revalidate',
197
        'no-store',
198
        'no-transform',
199
    ];
200
201
    /**
202
     * Get current vary keys
203
     *
204
     * @return array
205
     */
206
    public function getVary()
207
    {
208
        // Explicitly set vary
209
        if (isset($this->vary)) {
210
            return $this->vary;
211
        }
212
213
        // Load default from config
214
        $defaultVary = $this->config()->get('defaultVary');
215
        return array_keys(array_filter($defaultVary));
216
    }
217
218
    /**
219
     * Add a vary
220
     *
221
     * @param string|array $vary
222
     * @return $this
223
     */
224
    public function addVary($vary)
225
    {
226
        $combied = $this->combineVary($this->getVary(), $vary);
227
        $this->setVary($combied);
228
        return $this;
229
    }
230
231
    /**
232
     * Set vary
233
     *
234
     * @param array|string $vary
235
     * @return $this
236
     */
237
    public function setVary($vary)
238
    {
239
        $this->vary = $this->combineVary($vary);
240
        return $this;
241
    }
242
243
    /**
244
     * Combine vary strings/arrays into a single array, or normalise a single vary
245
     *
246
     * @param string|array[] $varies Each vary as a separate arg
247
     * @return array
248
     */
249
    protected function combineVary(...$varies)
250
    {
251
        $merged = [];
252
        foreach ($varies as $vary) {
253
            if ($vary && is_string($vary)) {
254
                $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

254
                $vary = array_filter(/** @scrutinizer ignore-type */ preg_split("/\s*,\s*/", trim($vary)));
Loading history...
255
            }
256
            if ($vary && is_array($vary)) {
257
                $merged = array_merge($merged, $vary);
258
            }
259
        }
260
        return array_unique($merged);
261
    }
262
263
264
    /**
265
     * Register a modification date. Used to calculate the "Last-Modified" HTTP header.
266
     * Can be called multiple times, and will automatically retain the most recent date.
267
     *
268
     * @param string|int $date Date string or timestamp
269
     * @return HTTPCacheControlMiddleware
270
     */
271
    public function registerModificationDate($date)
272
    {
273
        $timestamp = is_numeric($date) ? $date : strtotime($date);
274
        if ($timestamp > $this->modificationDate) {
275
            $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...
276
        }
277
        return $this;
278
    }
279
280
    /**
281
     * Set current state. Should only be invoked internally after processing precedence rules.
282
     *
283
     * @param string $state
284
     * @return $this
285
     */
286
    protected function setState($state)
287
    {
288
        if (!array_key_exists($state, $this->stateDirectives)) {
289
            throw new InvalidArgumentException("Invalid state {$state}");
290
        }
291
        $this->state = $state;
292
        return $this;
293
    }
294
295
    /**
296
     * Get current state
297
     *
298
     * @return string
299
     */
300
    public function getState()
301
    {
302
        return $this->state ?: $this->config()->get('defaultState');
303
    }
304
305
    /**
306
     * Instruct the cache to apply a change with a given level, optionally
307
     * modifying it with a force flag to increase priority of this action.
308
     *
309
     * If the apply level was successful, the change is made and the internal level
310
     * threshold is incremented.
311
     *
312
     * @param int $level Priority of the given change
313
     * @param bool $force If usercode has requested this action is forced to a higher priority.
314
     * Note: Even if $force is set to true, other higher-priority forced changes can still
315
     * cause a change to be rejected if it is below the required threshold.
316
     * @return bool True if the given change is accepted, and that the internal
317
     * level threshold is updated (if necessary) to the new minimum level.
318
     */
319
    protected function applyChangeLevel($level, $force)
320
    {
321
        $forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0);
322
        if ($forcingLevel < $this->getForcingLevel()) {
323
            return false;
324
        }
325
        $this->forcingLevel = $forcingLevel;
326
        return true;
327
    }
328
329
    /**
330
     * Low level method for setting directives include any experimental or custom ones added via config.
331
     * You need to specify the state (or states) to apply this directive to.
332
     * Can also remove directives with false
333
     *
334
     * @param array|string $states State(s) to apply this directive to
335
     * @param string $directive
336
     * @param int|string|bool $value Flag to set for this value. Set to false to remove, or true to set.
337
     * String or int value assign a specific value.
338
     * @return $this
339
     */
340
    public function setStateDirective($states, $directive, $value = true)
341
    {
342
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always false.
Loading history...
343
            throw new InvalidArgumentException("Invalid directive value");
344
        }
345
        // make sure the directive is in the list of allowed directives
346
        $allowedDirectives = $this->config()->get('allowed_directives');
347
        $directive = strtolower($directive);
348
        if (!in_array($directive, $allowedDirectives)) {
349
            throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed');
350
        }
351
        foreach ((array)$states as $state) {
352
            if (!array_key_exists($state, $this->stateDirectives)) {
353
                throw new InvalidArgumentException("Invalid state {$state}");
354
            }
355
            // Set or unset directive
356
            if ($value === false) {
357
                unset($this->stateDirectives[$state][$directive]);
358
            } else {
359
                $this->stateDirectives[$state][$directive] = $value;
360
            }
361
        }
362
        return $this;
363
    }
364
365
    /**
366
     * Low level method to set directives from an associative array
367
     *
368
     * @param array|string $states State(s) to apply this directive to
369
     * @param array $directives
370
     * @return $this
371
     */
372
    public function setStateDirectivesFromArray($states, $directives)
373
    {
374
        foreach ($directives as $directive => $value) {
375
            $this->setStateDirective($states, $directive, $value);
376
        }
377
        return $this;
378
    }
379
380
    /**
381
     * Low level method for removing directives
382
     *
383
     * @param array|string $states State(s) to remove this directive from
384
     * @param string $directive
385
     * @return $this
386
     */
387
    public function removeStateDirective($states, $directive)
388
    {
389
        $this->setStateDirective($states, $directive, false);
390
        return $this;
391
    }
392
393
    /**
394
     * Low level method to check if a directive is currently set
395
     *
396
     * @param string $state State(s) to apply this directive to
397
     * @param string $directive
398
     * @return bool
399
     */
400
    public function hasStateDirective($state, $directive)
401
    {
402
        $directive = strtolower($directive);
403
        return isset($this->stateDirectives[$state][$directive]);
404
    }
405
406
    /**
407
     * Check if the current state has the given directive.
408
     *
409
     * @param string $directive
410
     * @return bool
411
     */
412
    public function hasDirective($directive)
413
    {
414
        return $this->hasStateDirective($this->getState(), $directive);
415
    }
416
417
    /**
418
     * Low level method to get the value of a directive for a state.
419
     * Returns false if there is no directive.
420
     * True means the flag is set, otherwise the value of the directive.
421
     *
422
     * @param string $state
423
     * @param string $directive
424
     * @return int|string|bool
425
     */
426
    public function getStateDirective($state, $directive)
427
    {
428
        $directive = strtolower($directive);
429
        if (isset($this->stateDirectives[$state][$directive])) {
430
            return $this->stateDirectives[$state][$directive];
431
        }
432
        return false;
433
    }
434
435
    /**
436
     * Get the value of the given directive for the current state
437
     *
438
     * @param string $directive
439
     * @return bool|int|string
440
     */
441
    public function getDirective($directive)
442
    {
443
        return $this->getStateDirective($this->getState(), $directive);
444
    }
445
446
    /**
447
     * Get directives for the given state
448
     *
449
     * @param string $state
450
     * @return array
451
     */
452
    public function getStateDirectives($state)
453
    {
454
        return $this->stateDirectives[$state];
455
    }
456
457
    /**
458
     * Get all directives for the currently active state
459
     *
460
     * @return array
461
     */
462
    public function getDirectives()
463
    {
464
        return $this->getStateDirectives($this->getState());
465
    }
466
467
    /**
468
     * The cache should not store anything about the client request or server response.
469
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
470
     * Set the no-store directive (also removes max-age and s-maxage for consistency purposes)
471
     *
472
     * @param bool $noStore
473
     *
474
     * @return $this
475
     */
476
    public function setNoStore($noStore = true)
477
    {
478
        // Affect all non-disabled states
479
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
480
        if ($noStore) {
481
            $this->setStateDirective($applyTo, 'no-store');
482
            $this->removeStateDirective($applyTo, 'max-age');
483
            $this->removeStateDirective($applyTo, 's-maxage');
484
        } else {
485
            $this->removeStateDirective($applyTo, 'no-store');
486
        }
487
        return $this;
488
    }
489
490
    /**
491
     * Forces caches to submit the request to the origin server for validation before releasing a cached copy.
492
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
493
     *
494
     * @param bool $noCache
495
     * @return $this
496
     */
497
    public function setNoCache($noCache = true)
498
    {
499
        // Affect all non-disabled states
500
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
501
        $this->setStateDirective($applyTo, 'no-cache', $noCache);
502
        return $this;
503
    }
504
505
    /**
506
     * Specifies the maximum amount of time (seconds) a resource will be considered fresh.
507
     * This directive is relative to the time of the request.
508
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
509
     *
510
     * @param int $age
511
     * @return $this
512
     */
513
    public function setMaxAge($age)
514
    {
515
        // Affect all non-disabled states
516
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
517
        $this->setStateDirective($applyTo, 'max-age', $age);
518
        return $this;
519
    }
520
521
    /**
522
     * Overrides max-age or the Expires header, but it only applies to shared caches (e.g., proxies)
523
     * and is ignored by a private cache.
524
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
525
     *
526
     * @param int $age
527
     * @return $this
528
     */
529
    public function setSharedMaxAge($age)
530
    {
531
        // Affect all non-disabled states
532
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
533
        $this->setStateDirective($applyTo, 's-maxage', $age);
534
        return $this;
535
    }
536
537
    /**
538
     * The cache must verify the status of the stale resources before using it and expired ones should not be used.
539
     * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
540
     *
541
     * @param bool $mustRevalidate
542
     * @return $this
543
     */
544
    public function setMustRevalidate($mustRevalidate = true)
545
    {
546
        $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
547
        $this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate);
548
        return $this;
549
    }
550
551
    /**
552
     * Simple way to set cache control header to a cacheable state.
553
     *
554
     * The resulting cache-control headers will be chosen from the 'enabled' set of directives.
555
     *
556
     * Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required.
557
     * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
558
     *
559
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
560
     * @param bool $force Force the cache to public even if its unforced private or public
561
     * @return $this
562
     */
563
    public function enableCache($force = false)
564
    {
565
        // Only execute this if its forcing level is high enough
566
        if ($this->applyChangeLevel(self::LEVEL_ENABLED, $force)) {
567
            $this->setState(self::STATE_ENABLED);
568
        }
569
        return $this;
570
    }
571
572
    /**
573
     * Simple way to set cache control header to a non-cacheable state.
574
     * Use this method over `privateCache()` if you are unsure about caching details.
575
     * Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
576
     *
577
     * The resulting cache-control headers will be chosen from the 'disabled' set of directives.
578
     *
579
     * Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
580
     * the others are added under recommendation from Mozilla (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
581
     *
582
     * Does not set `private` directive, use `privateCache()` if this is explicitly required.
583
     * See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
584
     *
585
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
586
     * @param bool $force Force the cache to diabled even if it's forced private or public
587
     * @return $this
588
     */
589
    public function disableCache($force = false)
590
    {
591
        // Only execute this if its forcing level is high enough
592
        if ($this->applyChangeLevel(self::LEVEL_DISABLED, $force)) {
593
            $this->setState(self::STATE_DISABLED);
594
        }
595
        return $this;
596
    }
597
598
    /**
599
     * Advanced way to set cache control header to a non-cacheable state.
600
     * Indicates that the response is intended for a single user and must not be stored by a shared cache.
601
     * A private cache (e.g. Web Browser) may store the response.
602
     *
603
     * The resulting cache-control headers will be chosen from the 'private' set of directives.
604
     *
605
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
606
     * @param bool $force Force the cache to private even if it's forced public
607
     * @return $this
608
     */
609
    public function privateCache($force = false)
610
    {
611
        // Only execute this if its forcing level is high enough
612
        if ($this->applyChangeLevel(self::LEVEL_PRIVATE, $force)) {
613
            $this->setState(self::STATE_PRIVATE);
614
        }
615
        return $this;
616
    }
617
618
    /**
619
     * Advanced way to set cache control header to a cacheable state.
620
     * Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
621
     *
622
     * The resulting cache-control headers will be chosen from the 'private' set of directives.
623
     *
624
     * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
625
     * @param bool $force Force the cache to public even if it's private, unless it's been forced private
626
     * @return $this
627
     */
628
    public function publicCache($force = false)
629
    {
630
        // Only execute this if its forcing level is high enough
631
        if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
632
            $this->setState(self::STATE_PUBLIC);
633
        }
634
        return $this;
635
    }
636
637
    /**
638
     * Generate all headers to add to this object
639
     *
640
     * @param HTTPResponse $response
641
     *
642
     * @return $this
643
     */
644
    public function applyToResponse($response)
645
    {
646
        $headers = $this->generateHeadersFor($response);
647
        foreach ($headers as $name => $value) {
648
            if (!$response->getHeader($name)) {
649
                $response->addHeader($name, $value);
650
            }
651
        }
652
        return $this;
653
    }
654
655
    /**
656
     * Generate the cache header
657
     *
658
     * @return string
659
     */
660
    protected function generateCacheHeader()
661
    {
662
        $cacheControl = [];
663
        foreach ($this->getDirectives() as $directive => $value) {
664
            if ($value === true) {
665
                $cacheControl[] = $directive;
666
            } else {
667
                $cacheControl[] = $directive . '=' . $value;
668
            }
669
        }
670
        return implode(', ', $cacheControl);
671
    }
672
673
    /**
674
     * Generate all headers to output
675
     *
676
     * @param HTTPResponse $response
677
     * @return array
678
     */
679
    public function generateHeadersFor(HTTPResponse $response)
680
    {
681
        return array_filter([
682
            'Last-Modified' => $this->generateLastModifiedHeader(),
683
            'Vary' => $this->generateVaryHeader($response),
684
            'Cache-Control' => $this->generateCacheHeader(),
685
            'Expires' => $this->generateExpiresHeader(),
686
        ]);
687
    }
688
689
    /**
690
     * Reset registered http cache control and force a fresh instance to be built
691
     */
692
    public static function reset()
693
    {
694
        Injector::inst()->unregisterNamedObject(__CLASS__);
695
    }
696
697
    /**
698
     * @return int
699
     */
700
    protected function getForcingLevel()
701
    {
702
        if (isset($this->forcingLevel)) {
703
            return $this->forcingLevel;
704
        }
705
        return $this->config()->get('defaultForcingLevel');
706
    }
707
708
    /**
709
     * Generate vary http header
710
     *
711
     * @param HTTPResponse $response
712
     * @return string|null
713
     */
714
    protected function generateVaryHeader(HTTPResponse $response)
715
    {
716
        // split the current vary header into it's parts and merge it with the config settings
717
        // to create a list of unique vary values
718
        $vary = $this->getVary();
719
        if ($response->getHeader('Vary')) {
720
            $vary = $this->combineVary($vary, $response->getHeader('Vary'));
721
        }
722
        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...
723
            return implode(', ', $vary);
724
        }
725
        return null;
726
    }
727
728
    /**
729
     * Generate Last-Modified header
730
     *
731
     * @return string|null
732
     */
733
    protected function generateLastModifiedHeader()
734
    {
735
        if (!$this->modificationDate) {
736
            return null;
737
        }
738
        return gmdate('D, d M Y H:i:s', $this->modificationDate) . ' GMT';
739
    }
740
741
    /**
742
     * Generate Expires http header
743
     *
744
     * @return null|string
745
     */
746
    protected function generateExpiresHeader()
747
    {
748
        $maxAge = $this->getDirective('max-age');
749
        if ($maxAge === false) {
750
            return null;
751
        }
752
753
        // Add now to max-age to generate expires
754
        $expires = DBDatetime::now()->getTimestamp() + $maxAge;
755
        return gmdate('D, d M Y H:i:s', $expires) . ' GMT';
756
    }
757
758
    /**
759
     * Update state based on current request and response objects
760
     *
761
     * @param HTTPRequest $request
762
     * @param HTTPResponse $response
763
     */
764
    protected function augmentState(HTTPRequest $request, HTTPResponse $response)
765
    {
766
        // If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are
767
        // likely to be supplying information relevant to the current user only
768
        if ($request->getSession()->getAll()) {
769
            // Don't force in case user code chooses to opt in to public caching
770
            $this->privateCache();
771
        }
772
773
        // Errors disable cache (unless some errors are cached intentionally by usercode)
774
        if ($response->isError() || $response->isRedirect()) {
775
            // Even if publicCache(true) is specified, errors will be uncacheable
776
            $this->disableCache(true);
777
        }
778
    }
779
}
780