check_dl_hit()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5.1971

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 12
ccs 3
cts 8
cp 0.375
crap 5.1971
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.services
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
use Symfony\Component\HttpFoundation\Response;
10
use Symfony\Component\HttpFoundation\Request;
11
use Symfony\Component\HttpFoundation\BinaryFileResponse;
12
use Symfony\Component\HttpKernel\Event\ResponseEvent;
13
use Symfony\Component\HttpKernel\Event\RequestEvent;
14
use Symfony\Component\Cache\Adapter\AdapterInterface;
15
16
/**
17
 * This is the Output Caching Engine of MidCOM. It will intercept page output,
18
 * map it using the currently used URL and use the cached output on subsequent
19
 * requests.
20
 *
21
 * <b>Important note for application developers</b>
22
 *
23
 * Please read the documentation of the following functions thoroughly:
24
 *
25
 * - midcom_services_cache_module_content::no_cache();
26
 * - midcom_services_cache_module_content::uncached();
27
 * - midcom_services_cache_module_content::expires();
28
 * - midcom_services_cache_module_content::invalidate_all();
29
 * - midcom_services_cache_module_content::enable_live_mode();
30
 *
31
 * You have to use these functions everywhere where it is applicable or the cache
32
 * will not work reliably.
33
 *
34
 * <b>Caching strategy</b>
35
 *
36
 * The cache takes three parameters into account when storing in or retrieving from
37
 * the cache: The current User ID, the current language and the request's URL.
38
 *
39
 * Only on a complete match a cached page is displayed, which should take care of any
40
 * permission checks done on the page. When you change the permissions of users, you
41
 * need to manually invalidate the cache though, as MidCOM currently cannot detect
42
 * changes like this (of course, this is true if and only if you are not using a
43
 * MidCOM to change permissions).
44
 *
45
 * When the HTTP request is not cacheable, the caching engine will automatically and
46
 * transparently go into no_cache mode for that request only. This feature
47
 * does neither invalidate the cache or drop the page that would have been delivered
48
 * normally from the cache. If you change the content, you need to do that yourself.
49
 *
50
 * HTTP 304 Not Modified support is built into this module, and will send a 304 reply if applicable.
51
 *
52
 * <b>Module configuration (see also midcom_config)</b>
53
 *
54
 * - <i>boolean cache_module_content_uncached</i>: Set this to true to prevent the saving of cached pages. This is useful
55
 *   for development work, as all other headers (like E-Tag or Last-Modified) are generated
56
 *   normally. See the uncached() and _uncached members.
57
 *
58
 * @package midcom.services
59
 */
60
class midcom_services_cache_module_content extends midcom_services_cache_module
61
{
62
    /**
63
     * Flag, indicating whether the current page may be cached. If
64
     * false, the usual no-cache headers will be generated.
65
     */
66
    private bool $_no_cache = false;
67
68
    /**
69
     * An array storing all HTTP headers registered through register_sent_header().
70
     * They will be sent when a cached page is delivered.
71
     */
72
    private array $_sent_headers = [];
73
74
    /**
75
     * Set this to true if you want to inhibit storage of the generated pages in
76
     * the cache database. All other headers will be created as usual though, so
77
     * 304 processing will kick in for example.
78
     */
79
    private bool $_uncached = false;
80
81
    /**
82
     * Controls cache headers strategy
83
     * 'no-cache' activates no-cache mode that actively tries to circumvent all caching
84
     * 'revalidate' is the default which sets must-revalidate. Expiry defaults to current time, so this effectively behaves like no-cache if expires() was not called
85
     * 'public' and 'private' enable caching with the cache-control header of the same name, default expiry timestamps are generated using the default_lifetime
86
     */
87
    private string $_headers_strategy = 'revalidate';
88
89
    /**
90
     * Controls cache headers strategy for authenticated users, needed because some proxies store cookies, too,
91
     * making a horrible mess when used by mix of authenticated and non-authenticated users
92
     *
93
     * @see $_headers_strategy
94
     */
95
    private string $_headers_strategy_authenticated = 'private';
96
97
    /**
98
     * Default lifetime of page for public/private headers strategy
99
     * When generating the default expires header this is added to time().
100
     */
101
    private int $_default_lifetime = 0;
102
103
    /**
104
     * Default lifetime of page for public/private headers strategy for authenticated users
105
     *
106
     * @see $_default_lifetime
107
     */
108
    private int $_default_lifetime_authenticated = 0;
109
110
    /**
111
     * A cache backend used to store the actual cached pages.
112
     */
113
    private AdapterInterface $_data_cache;
114
115
    /**
116
     * GUIDs loaded per context in this request
117
     */
118
    private array $context_guids = [];
119
120
    private midcom_config $config;
121
122
    /**
123
     * Initialize the cache.
124
     *
125
     * The first step is to initialize the cache backends. The names of the
126
     * cache backends used for meta and data storage are derived from the name
127
     * defined for this module (see the 'name' configuration parameter above).
128
     * The name is used directly for the meta data cache, while the actual data
129
     * is stored in a backend postfixed with '_data'.
130
     *
131
     * After core initialization, the module checks for a cache hit (which might
132
     * trigger the delivery of the cached page and exit) and start the output buffer
133
     * afterwards.
134
     */
135 2
    public function __construct(midcom_config $config, AdapterInterface $backend, AdapterInterface $data_cache)
136
    {
137 2
        parent::__construct($backend);
138 2
        $this->config = $config;
139 2
        $this->_data_cache = $data_cache;
140
141 2
        $this->_uncached = $config->get('cache_module_content_uncached');
142 2
        $this->_headers_strategy = $this->get_strategy('cache_module_content_headers_strategy');
143 2
        $this->_headers_strategy_authenticated = $this->get_strategy('cache_module_content_headers_strategy_authenticated');
144 2
        $this->_default_lifetime = (int)$config->get('cache_module_content_default_lifetime');
145 2
        $this->_default_lifetime_authenticated = (int)$config->get('cache_module_content_default_lifetime_authenticated');
146
147 2
        if ($this->_headers_strategy == 'no-cache') {
148
            // we can't call no_cache() here, because it would try to call back to this class via the global getter
149
            $header = 'Cache-Control: no-store, no-cache, must-revalidate';
150
            $this->register_sent_header($header);
151
            midcom_compat_environment::header($header);
152
            $this->_no_cache = true;
153
        }
154
    }
155
156 353
    public function on_request(RequestEvent $event)
157
    {
158 353
        $request = $event->getRequest();
159 353
        if ($event->isMainRequest()) {
160
            /* Load and start up the cache system, this might already end the request
161
             * on a content cache hit. Note that the cache check hit depends on the i18n and auth code.
162
             */
163 1
            if ($response = $this->_check_hit($request)) {
164 1
                $event->setResponse($response);
165
            }
166 352
        } elseif ($content = $this->check_dl_hit($request)) {
167
            $event->setResponse(new Response($content));
168
        }
169
    }
170
171
    /**
172
     * This function holds the cache hit check mechanism. It searches the requested
173
     * URL in the cache database. If found, it checks, whether the cache page has
174
     * expired. If not, the response is returned. In all other cases this method simply
175
     * returns void.
176
     *
177
     * Also, any HTTP POST request will automatically circumvent the cache so that
178
     * any component can process the request. It will set no_cache automatically
179
     * to avoid any cache pages being overwritten by, for example, search results.
180
     *
181
     * Note, that HTTP GET is <b>not</b> checked this way, as GET requests can be
182
     * safely distinguished by their URL.
183
     *
184
     * @return void|Response
185
     */
186 1
    private function _check_hit(Request $request)
187
    {
188 1
        if (!$request->isMethodCacheable()) {
189
            debug_add('Request method is not cacheable, setting no_cache');
190
            $this->no_cache();
191
            return;
192
        }
193
194
        // Check for uncached operation
195 1
        if ($this->_uncached) {
196
            debug_add("Uncached mode");
197
            return;
198
        }
199
200
        // Check that we have cache for the identifier
201 1
        $request_id = $this->generate_request_identifier($request);
202
        // Load metadata for the content identifier connected to current request
203 1
        $content_id = $this->backend->getItem($request_id);
204 1
        if (!$content_id->isHit()) {
205 1
            debug_add("MISS {$request_id}");
206
            // We have no information about content cached for this request
207 1
            return;
208
        }
209 1
        $content_id = $content_id->get();
210 1
        debug_add("HIT {$request_id}");
211
212 1
        $headers = $this->backend->getItem($content_id);
213 1
        if (!$headers->isHit()) {
214
            debug_add("MISS meta_cache {$content_id}");
215
            // Content cache data is missing
216
            return;
217
        }
218
219 1
        debug_add("HIT {$content_id}");
220
221 1
        $response = new Response('', Response::HTTP_OK, $headers->get());
222 1
        if (!$response->isNotModified($request)) {
223 1
            $content = $this->_data_cache->getItem($content_id);
224 1
            if (!$content->isHit()) {
225
                debug_add("Current page is in not in the data cache, possible ghost read.", MIDCOM_LOG_WARN);
226
                return;
227
            }
228 1
            $response->setContent($content->get());
229
        }
230
        // disable cache writing in on_response
231 1
        $this->_no_cache = true;
232 1
        return $response;
233
    }
234
235
    /**
236
     * This completes the output caching, post-processes it and updates the cache databases accordingly.
237
     *
238
     * The first step is to check against _no_cache pages, which will be delivered immediately
239
     * without any further post processing. Afterwards, the system will complete the sent
240
     * headers by adding all missing headers. Note, that E-Tag will be generated always
241
     * automatically, you must not set this in your component.
242
     *
243
     * If the midcom configuration option cache_uncached is set or the corresponding runtime function
244
     * has been called, the cache file will not be written, but the header stuff will be added like
245
     * usual to allow for browser-side caching.
246
     *
247
     * @param ResponseEvent $event The request object
248
     */
249 354
    public function on_response(ResponseEvent $event)
250
    {
251 354
        $response = $event->getResponse();
252 354
        $request = $event->getRequest();
253 354
        if ($response->isServerError()) {
254 2
            return;
255
        }
256 353
        if (!$event->isMainRequest()) {
257 352
            $this->store_dl_content($request, $response);
258 352
            return;
259
        }
260 1
        if ($response instanceof BinaryFileResponse) {
261
            $this->cache_control_headers($response);
262
            // Store metadata in cache so _check_hit() can help us
263
            $this->write_meta_cache('A-' . $response->getEtag(), $event->getRequest(), $response);
264
            return;
265
        }
266 1
        foreach ($this->_sent_headers as $header => $value) {
267
            // This can happen in streamed responses which enable_live_mode
268
            if (!headers_sent()) {
269
                header_remove($header);
270
            }
271
            $response->headers->set($header, $value);
272
        }
273 1
        if ($this->_no_cache) {
274
            $response->prepare($request);
275
            return;
276
        }
277
278 1
        $cache_data = $response->getContent();
279
280
        // Register additional Headers around the current output request
281 1
        $this->complete_sent_headers($response);
282 1
        $response->prepare($request);
283
284
        // Generate E-Tag header.
285 1
        if (empty($cache_data)) {
286
            $etag = md5(serialize($response->headers->all()));
287
        } else {
288 1
            $etag = md5($cache_data);
289
        }
290 1
        $response->setEtag($etag);
291
292 1
        if ($this->_uncached) {
293
            debug_add('Not writing cache file, we are in uncached operation mode.');
294
            return;
295
        }
296 1
        $content_id = 'C-' . $etag;
297 1
        $this->write_meta_cache($content_id, $request, $response);
298 1
        $item = $this->_data_cache->getItem($content_id);
299 1
        $this->_data_cache->save($item->set($cache_data));
300
    }
301
302
    /**
303
     * Generate a valid cache identifier for a context of the current request
304
     */
305 2
    private function generate_request_identifier(Request $request) : string
306
    {
307 2
        $context = $request->attributes->get('context')->id;
308
        // Cache the request identifier so that it doesn't change between start and end of request
309 2
        static $identifier_cache = [];
310 2
        if (isset($identifier_cache[$context])) {
311 1
            return $identifier_cache[$context];
312
        }
313
314 2
        $identifier_source = '';
315
316 2
        $cache_strategy = $this->config->get('cache_module_content_caching_strategy');
317
318
        switch ($cache_strategy) {
319 2
            case 'memberships':
320
                if (!midcom::get()->auth->is_valid_user()) {
321
                    $identifier_source .= 'USER=ANONYMOUS';
322
                    break;
323
                }
324
325
                $mc = new midgard_collector('midgard_member', 'uid', midcom_connection::get_user());
326
                $mc->set_key_property('gid');
327
                $mc->execute();
328
                $gids = $mc->list_keys();
329
                $identifier_source .= 'GROUPS=' . implode(',', array_keys($gids));
330
                break;
331 2
            case 'public':
332
                $identifier_source .= 'USER=EVERYONE';
333
                break;
334 2
            case 'user':
335
            default:
336 2
                $identifier_source .= 'USER=' . midcom_connection::get_user();
337 2
                break;
338
        }
339
340 2
        $identifier_source .= ';URL=' . $request->getUri();
341 2
        debug_add("Generating context {$context} request-identifier from: {$identifier_source}");
342
343 2
        $identifier_cache[$context] = 'R-' . md5($identifier_source);
344 2
        return $identifier_cache[$context];
345
    }
346
347 2
    private function get_strategy(string $name) : string
348
    {
349 2
        $strategy = strtolower($this->config->get($name));
0 ignored issues
show
Bug introduced by
It seems like $this->config->get($name) can also be of type null; however, parameter $string of strtolower() does only seem to accept string, 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

349
        $strategy = strtolower(/** @scrutinizer ignore-type */ $this->config->get($name));
Loading history...
350 2
        $allowed = ['no-cache', 'revalidate', 'public', 'private'];
351 2
        if (!in_array($strategy, $allowed)) {
352
            throw new midcom_error($name . ' is not valid, try ' . implode(', ', $allowed));
353
        }
354 2
        return $strategy;
355
    }
356
357
    /**
358
     * Call this, if the currently processed output must not be cached for any
359
     * reason. Dynamic pages with sensitive content are a candidate for this
360
     * function.
361
     *
362
     * Note, that this will prevent <i>any</i> content invalidation related headers
363
     * like E-Tag to be generated automatically, and that the appropriate
364
     * no-store/no-cache headers from HTTP 1.1 and HTTP 1.0 will be sent automatically.
365
     * This means that there will also be no 304 processing.
366
     *
367
     * You should use this only for sensitive content. For simple dynamic output,
368
     * you are strongly encouraged to use the less strict uncached() function.
369
     *
370
     * @see uncached()
371
     */
372 199
    public function no_cache(?Response $response = null)
373
    {
374 199
        $settings = 'no-store, no-cache, must-revalidate';
375
        // PONDER: Send expires header (set to long time in past) as well ??
376
377 199
        if ($response) {
378
            $response->headers->set('Cache-Control', $settings);
379 199
        } elseif (!$this->_no_cache) {
380
            if (headers_sent()) {
381
                debug_add('Warning, we should move to no_cache but headers have already been sent, skipping header transmission.', MIDCOM_LOG_ERROR);
382
            } else {
383
                midcom::get()->header('Cache-Control: ' . $settings);
384
            }
385
        }
386 199
        $this->_no_cache = true;
387
    }
388
389
    /**
390
     * Call this, if the currently processed output must not be cached for any
391
     * reason. Dynamic pages or form processing results are the usual candidates
392
     * for this mode.
393
     *
394
     * Note, that this will still keep the caching engine active so that it can
395
     * add the usual headers (ETag, Expires ...) in respect to the no_cache flag.
396
     * As well, at the end of the processing, the usual 304 checks are done, so if
397
     * your page doesn't change in respect of E-Tag and Last-Modified, only a 304
398
     * Not Modified reaches the client.
399
     *
400
     * Essentially, no_cache behaves the same way as if the uncached configuration
401
     * directive is set to true, it is just limited to a single request.
402
     *
403
     * If you need a higher level of client side security, to avoid storage of sensitive
404
     * information on the client side, you should use no_cache instead.
405
     *
406
     * @see no_cache()
407
     */
408 4
    public function uncached(bool $uncached = true)
409
    {
410 4
        $this->_uncached = $uncached;
411
    }
412
413
    /**
414
     * Put the cache into a "live mode". This will disable the
415
     * cache during runtime, correctly flushing the output buffer (if it's not empty)
416
     * and sending cache control headers.
417
     *
418
     * The midcom-exec URL handler of the core will automatically enable live mode.
419
     *
420
     * @see midcom_application::_exec_file()
421
     */
422
    public function enable_live_mode()
423
    {
424
        $this->no_cache();
425
        Response::closeOutputBuffers(0, ob_get_length() > 0);
426
    }
427
428
    /**
429
     * Store a sent header into the cache database, so that it will
430
     * be resent when the cache page is delivered. midcom_application::header()
431
     * will automatically call this function, you need to do this only if you use
432
     * the PHP header function.
433
     */
434 17
    public function register_sent_header(string $header)
435
    {
436 17
        if (str_contains($header, ': ')) {
437 17
            [$header, $value] = explode(': ', $header, 2);
438 17
            $this->_sent_headers[$header] = $value;
439
        }
440
    }
441
442
    /**
443
     * Looks for list of content and request identifiers paired with the given guid
444
     * and removes all of those from the caches.
445
     *
446
     * {@inheritDoc}
447
     */
448 321
    public function invalidate(string $guid, ?midcom_core_dbaobject $object = null)
449
    {
450 321
        $guidmap = $this->backend->getItem($guid);
451 321
        if (!$guidmap->isHit()) {
452 321
            debug_add("No entry for {$guid} in meta cache, ignoring invalidation request.");
453 321
            return;
454
        }
455
456
        foreach ($guidmap->get() as $content_id) {
457
            $this->backend->deleteItem($content_id);
458
            $this->_data_cache->deleteItem($content_id);
459
        }
460
    }
461
462
    public function invalidate_all()
463
    {
464
        parent::invalidate_all();
465
        $this->_data_cache->clear();
466
    }
467
468
    /**
469
     * All objects loaded within a request are stored into a list for cache invalidation purposes
470
     */
471 410
    public function register(string $guid)
472
    {
473
        // Check for uncached operation
474 410
        if ($this->_uncached) {
475 409
            return;
476
        }
477
478 1
        $context = midcom_core_context::get()->id;
479 1
        if ($context != 0) {
480
            // We're in a dynamic_load, register it for that as well
481 1
            $this->context_guids[$context] ??= [];
482 1
            $this->context_guids[$context][] = $guid;
483
        }
484
485
        // Register all GUIDs also to the root context
486 1
        $this->context_guids[0] ??= [];
487 1
        $this->context_guids[0][] = $guid;
488
    }
489
490
    /**
491
     * Writes meta-cache entry from context data using given content id
492
     * Used to be part of on_request, but needed by serve-attachment method in midcom_core_urlmethods as well
493
     */
494 1
    private function write_meta_cache(string $content_id, Request $request, Response $response)
495
    {
496 1
        if (   $this->_uncached
497 1
            || $this->_no_cache) {
498
            return;
499
        }
500
501
        // Construct cache identifier
502 1
        $request_id = $this->generate_request_identifier($request);
503
504 1
        $item = $this->backend->getItem($request_id);
505 1
        $this->backend->save($item->set($content_id));
506 1
        $item2 = $this->backend->getItem($content_id);
507 1
        $this->backend->save($item2->set($response->headers->all()));
508
509
        // Cache where the object have been
510 1
        $this->store_context_guid_map($content_id, $request_id);
511
    }
512
513 2
    private function store_context_guid_map(string $content_id, string $request_id)
514
    {
515 2
        $context = midcom_core_context::get()->id;
516
        // non-existent context
517 2
        if (!array_key_exists($context, $this->context_guids)) {
518 1
            return;
519
        }
520
521 1
        $maps = $this->backend->getItems($this->context_guids[$context]);
522 1
        if ($maps instanceof Traversable) {
523 1
            $maps = iterator_to_array($maps);
524
        }
525 1
        $to_save = [];
526 1
        foreach ($this->context_guids[$context] as $guid) {
527 1
            $guidmap = $maps[$guid]->get() ?? [];
528
529 1
            if (!in_array($content_id, $guidmap)) {
530 1
                $guidmap[] = $content_id;
531 1
                $to_save[$guid] = $maps[$guid]->set($guidmap);
532
            }
533
534 1
            if (!in_array($request_id, $guidmap)) {
535 1
                $guidmap[] = $request_id;
536 1
                $to_save[$guid] = $maps[$guid]->set($guidmap);
537
            }
538
        }
539
540 1
        foreach ($to_save as $item) {
541 1
            $this->backend->save($item);
542
        }
543
    }
544
545 352
    private function check_dl_hit(Request $request)
546
    {
547 352
        if ($this->_no_cache) {
548 352
            return false;
549
        }
550
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
551
        $dl_content_id = $this->backend->getItem($dl_request_id);
552
        if (!$dl_content_id->isHit()) {
553
            return false;
554
        }
555
556
        return $this->_data_cache->getItem($dl_content_id->get())->get();
557
    }
558
559 352
    private function store_dl_content(Request $request, Response $response)
560
    {
561 352
        if (   $this->_no_cache
562 352
            || $this->_uncached) {
563 351
            return;
564
        }
565 1
        $dl_cache_data = $response->getContent();
566 1
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
567 1
        $dl_content_id = 'DLC-' . md5($dl_cache_data);
568
569 1
        $item = $this->backend->getItem($dl_request_id)
570 1
            ->set($dl_content_id)
571 1
            ->expiresAfter($this->_default_lifetime);
572 1
        $this->backend->save($item);
573 1
        $item = $this->_data_cache->getItem($dl_content_id)
574 1
            ->set($dl_cache_data)
575 1
            ->expiresAfter($this->_default_lifetime);
576 1
        $this->_data_cache->save($item);
577
578
        // Cache where the object have been
579 1
        $this->store_context_guid_map($dl_content_id, $dl_request_id);
580
    }
581
582
    /**
583
     * This little helper ensures that the headers Content-Length
584
     * and Last-Modified are present. The lastmod timestamp is taken out of the
585
     * component context information if it is populated correctly there; if not, the
586
     * system time is used instead.
587
     *
588
     * To force browsers to revalidate the page on every request (login changes would
589
     * go unnoticed otherwise), the Cache-Control header max-age=0 is added automatically.
590
     */
591 1
    private function complete_sent_headers(Response $response)
592
    {
593 1
        if (!$response->getLastModified()) {
594
            /* Determine Last-Modified using MidCOM's component context,
595
             * Fallback to time() if this fails.
596
             */
597 1
            $time = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_LASTMODIFIED) ?: time();
598 1
            $response->setLastModified(DateTime::createFromFormat('U', (string) $time));
599
        }
600
601
        /* TODO: Doublecheck the way this is handled, now we just don't send it
602
         * if headers_strategy implies caching */
603 1
        if (   !$response->headers->has('Content-Length')
604 1
            && !in_array($this->_headers_strategy, ['public', 'private'])) {
605 1
            $response->headers->set("Content-Length", strlen($response->getContent()));
606
        }
607
608 1
        $this->cache_control_headers($response);
609
    }
610
611 1
    private function cache_control_headers(Response $response)
612
    {
613
        // Just to be sure not to mess the headers sent by no_cache in case it was called
614 1
        if ($this->_no_cache) {
615
            $this->no_cache($response);
616
        } else {
617
            // Add Expiration and Cache Control headers
618 1
            $strategy = $this->_headers_strategy;
619 1
            $default_lifetime = $this->_default_lifetime;
620 1
            if (midcom::get()->auth->is_valid_user()) {
621
                $strategy = $this->_headers_strategy_authenticated;
622
                $default_lifetime = $this->_default_lifetime_authenticated;
623
            }
624
625 1
            $now = $expires = time();
626 1
            if ($strategy != 'revalidate') {
627
                $expires += $default_lifetime;
628
                if ($strategy == 'private') {
629
                    $response->setPrivate();
630
                } else {
631
                    $response->setPublic();
632
                }
633
            }
634 1
            $max_age = $expires - $now;
635
636 1
            $response
637 1
                ->setExpires(DateTime::createFromFormat('U', $expires))
638 1
                ->setMaxAge($max_age);
639 1
            if ($max_age == 0) {
640 1
                $response->headers->addCacheControlDirective('must-revalidate');
641
            }
642
        }
643
    }
644
}
645