Passed
Push — master ( 3aa615...d28158 )
by Andreas
27:41
created

cache_control_headers()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.7999

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 13
nop 1
dl 0
loc 30
ccs 12
cts 19
cp 0.6316
crap 7.7999
rs 8.9617
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::content_type();
30
 * - midcom_services_cache_module_content::enable_live_mode();
31
 *
32
 * You have to use these functions everywhere where it is applicable or the cache
33
 * will not work reliably.
34
 *
35
 * <b>Caching strategy</b>
36
 *
37
 * The cache takes three parameters into account when storing in or retrieving from
38
 * the cache: The current User ID, the current language and the request's URL.
39
 *
40
 * Only on a complete match a cached page is displayed, which should take care of any
41
 * permission checks done on the page. When you change the permissions of users, you
42
 * need to manually invalidate the cache though, as MidCOM currently cannot detect
43
 * changes like this (of course, this is true if and only if you are not using a
44
 * MidCOM to change permissions).
45
 *
46
 * When the HTTP request is not cacheable, the caching engine will automatically and
47
 * transparently go into no_cache mode for that request only. This feature
48
 * does neither invalidate the cache or drop the page that would have been delivered
49
 * normally from the cache. If you change the content, you need to do that yourself.
50
 *
51
 * HTTP 304 Not Modified support is built into this module, and will send a 304 reply if applicable.
52
 *
53
 * <b>Module configuration (see also midcom_config)</b>
54
 *
55
 * - <i>boolean cache_module_content_uncached</i>: Set this to true to prevent the saving of cached pages. This is useful
56
 *   for development work, as all other headers (like E-Tag or Last-Modified) are generated
57
 *   normally. See the uncached() and _uncached members.
58
 *
59
 * @package midcom.services
60
 */
61
class midcom_services_cache_module_content extends midcom_services_cache_module
62
{
63
    /**
64
     * Flag, indicating whether the current page may be cached. If
65
     * false, the usual no-cache headers will be generated.
66
     *
67
     * @var boolean
68
     */
69
    private $_no_cache = false;
70
71
    /**
72
     * An array storing all HTTP headers registered through register_sent_header().
73
     * They will be sent when a cached page is delivered.
74
     *
75
     * @var array
76
     */
77
    private $_sent_headers = [];
78
79
    /**
80
     * Set this to true if you want to inhibit storage of the generated pages in
81
     * the cache database. All other headers will be created as usual though, so
82
     * 304 processing will kick in for example.
83
     *
84
     * @var boolean
85
     */
86
    private $_uncached = false;
87
88
    /**
89
     * Controls cache headers strategy
90
     * 'no-cache' activates no-cache mode that actively tries to circumvent all caching
91
     * '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
92
     * 'public' and 'private' enable caching with the cache-control header of the same name, default expiry timestamps are generated using the default_lifetime
93
     *
94
     * @var string
95
     */
96
    private $_headers_strategy = 'revalidate';
97
98
    /**
99
     * Controls cache headers strategy for authenticated users, needed because some proxies store cookies, too,
100
     * making a horrible mess when used by mix of authenticated and non-authenticated users
101
     *
102
     * @see $_headers_strategy
103
     * @var string
104
     */
105
    private $_headers_strategy_authenticated = 'private';
106
107
    /**
108
     * Default lifetime of page for public/private headers strategy
109
     * When generating the default expires header this is added to time().
110
     *
111
     * @var int
112
     */
113
    private $_default_lifetime = 0;
114
115
    /**
116
     * Default lifetime of page for public/private headers strategy for authenticated users
117
     *
118
     * @see $_default_lifetime
119
     * @var int
120
     */
121
    private $_default_lifetime_authenticated = 0;
122
123
    /**
124
     * A cache backend used to store the actual cached pages.
125
     *
126
     * @var AdapterInterface
127
     */
128
    private $_data_cache;
129
130
    /**
131
     * GUIDs loaded per context in this request
132
     */
133
    private $context_guids = [];
134
135
    /**
136
     * @var midcom_config
137
     */
138
    private $config;
139
140
    /**
141
     * Initialize the cache.
142
     *
143
     * The first step is to initialize the cache backends. The names of the
144
     * cache backends used for meta and data storage are derived from the name
145
     * defined for this module (see the 'name' configuration parameter above).
146
     * The name is used directly for the meta data cache, while the actual data
147
     * is stored in a backend postfixed with '_data'.
148
     *
149
     * After core initialization, the module checks for a cache hit (which might
150
     * trigger the delivery of the cached page and exit) and start the output buffer
151
     * afterwards.
152
     */
153 2
    public function __construct(midcom_config $config, AdapterInterface $backend, AdapterInterface $data_cache)
154
    {
155 2
        parent::__construct($backend);
156 2
        $this->config = $config;
157 2
        $this->_data_cache = $data_cache;
158
159 2
        $this->_uncached = $config->get('cache_module_content_uncached');
160 2
        $this->_headers_strategy = $this->get_strategy('cache_module_content_headers_strategy');
161 2
        $this->_headers_strategy_authenticated = $this->get_strategy('cache_module_content_headers_strategy_authenticated');
162 2
        $this->_default_lifetime = (int)$config->get('cache_module_content_default_lifetime');
163 2
        $this->_default_lifetime_authenticated = (int)$config->get('cache_module_content_default_lifetime_authenticated');
164
165 2
        if ($this->_headers_strategy == 'no-cache') {
166
            // we can't call no_cache() here, because it would try to call back to this class via the global getter
167
            $header = 'Cache-Control: no-store, no-cache, must-revalidate';
168
            $this->register_sent_header($header);
169
            midcom_compat_environment::header($header);
170
            $this->_no_cache = true;
171
        }
172 2
    }
173
174 346
    public function on_request(RequestEvent $event)
175
    {
176 346
        if ($event->isMasterRequest()) {
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Component\HttpKe...vent::isMasterRequest() has been deprecated: since symfony/http-kernel 5.3, use isMainRequest() instead ( Ignorable by Annotation )

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

176
        if (/** @scrutinizer ignore-deprecated */ $event->isMasterRequest()) {

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...
177 1
            $request = $event->getRequest();
178
            /* Load and start up the cache system, this might already end the request
179
             * on a content cache hit. Note that the cache check hit depends on the i18n and auth code.
180
             */
181 1
            if ($response = $this->_check_hit($request)) {
182 1
                $event->setResponse($response);
183
            }
184
        }
185 346
    }
186
187
    /**
188
     * This function holds the cache hit check mechanism. It searches the requested
189
     * URL in the cache database. If found, it checks, whether the cache page has
190
     * expired. If not, the response is returned. In all other cases this method simply
191
     * returns void.
192
     *
193
     * Also, any HTTP POST request will automatically circumvent the cache so that
194
     * any component can process the request. It will set no_cache automatically
195
     * to avoid any cache pages being overwritten by, for example, search results.
196
     *
197
     * Note, that HTTP GET is <b>not</b> checked this way, as GET requests can be
198
     * safely distinguished by their URL.
199
     *
200
     * @return void|Response
201
     */
202 1
    private function _check_hit(Request $request)
203
    {
204 1
        if (!$request->isMethodCacheable()) {
205
            debug_add('Request method is not cacheable, setting no_cache');
206
            $this->no_cache();
207
            return;
208
        }
209
210
        // Check for uncached operation
211 1
        if ($this->_uncached) {
212
            debug_add("Uncached mode");
213
            return;
214
        }
215
216
        // Check that we have cache for the identifier
217 1
        $request_id = $this->generate_request_identifier($request);
218
        // Load metadata for the content identifier connected to current request
219 1
        $content_id = $this->backend->getItem($request_id);
220 1
        if (!$content_id->isHit()) {
221 1
            debug_add("MISS {$request_id}");
222
            // We have no information about content cached for this request
223 1
            return;
224
        }
225 1
        $content_id = $content_id->get();
226 1
        debug_add("HIT {$request_id}");
227
228 1
        $headers = $this->backend->getItem($content_id);
229 1
        if (!$headers->isHit()) {
230
            debug_add("MISS meta_cache {$content_id}");
231
            // Content cache data is missing
232
            return;
233
        }
234
235 1
        debug_add("HIT {$content_id}");
236
237 1
        $response = new Response('', Response::HTTP_OK, $headers->get());
238 1
        if (!$response->isNotModified($request)) {
239 1
            $content = $this->_data_cache->getItem($content_id);
240 1
            if (!$content->isHit()) {
241
                debug_add("Current page is in not in the data cache, possible ghost read.", MIDCOM_LOG_WARN);
242
                return;
243
            }
244 1
            $response->setContent($content->get());
245
        }
246
        // disable cache writing in on_response
247 1
        $this->_no_cache = true;
248 1
        return $response;
249
    }
250
251
    /**
252
     * This completes the output caching, post-processes it and updates the cache databases accordingly.
253
     *
254
     * The first step is to check against _no_cache pages, which will be delivered immediately
255
     * without any further post processing. Afterwards, the system will complete the sent
256
     * headers by adding all missing headers. Note, that E-Tag will be generated always
257
     * automatically, you must not set this in your component.
258
     *
259
     * If the midcom configuration option cache_uncached is set or the corresponding runtime function
260
     * has been called, the cache file will not be written, but the header stuff will be added like
261
     * usual to allow for browser-side caching.
262
     *
263
     * @param ResponseEvent $event The request object
264
     */
265 347
    public function on_response(ResponseEvent $event)
266
    {
267 347
        if (!$event->isMasterRequest()) {
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Component\HttpKe...vent::isMasterRequest() has been deprecated: since symfony/http-kernel 5.3, use isMainRequest() instead ( Ignorable by Annotation )

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

267
        if (!/** @scrutinizer ignore-deprecated */ $event->isMasterRequest()) {

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...
268 346
            return;
269
        }
270 1
        $response = $event->getResponse();
271 1
        if ($response instanceof BinaryFileResponse) {
272
            return;
273
        }
274 1
        foreach ($this->_sent_headers as $header => $value) {
275
            // This can happen in streamed responses which enable_live_mode
276
            if (!headers_sent()) {
277
                header_remove($header);
278
            }
279
            $response->headers->set($header, $value);
280
        }
281 1
        $request = $event->getRequest();
282 1
        if ($this->_no_cache) {
283
            $response->prepare($request);
284
            return;
285
        }
286
287 1
        $cache_data = $response->getContent();
288
289
        // Register additional Headers around the current output request
290 1
        $this->complete_sent_headers($response);
291 1
        $response->prepare($request);
292
293
        // Generate E-Tag header.
294 1
        if (empty($cache_data)) {
295
            $etag = md5(serialize($response->headers->all()));
296
        } else {
297 1
            $etag = md5($cache_data);
298
        }
299 1
        $response->setEtag($etag);
300
301 1
        if ($this->_uncached) {
302
            debug_add('Not writing cache file, we are in uncached operation mode.');
303
            return;
304
        }
305 1
        $content_id = 'C-' . $etag;
306 1
        $this->write_meta_cache($content_id, $request, $response);
307 1
        $item = $this->_data_cache->getItem($content_id);
308 1
        $this->_data_cache->save($item->set($cache_data));
309 1
    }
310
311
    /**
312
     * Generate a valid cache identifier for a context of the current request
313
     */
314 2
    private function generate_request_identifier(Request $request) : string
315
    {
316 2
        $context = $request->attributes->get('context')->id;
317
        // Cache the request identifier so that it doesn't change between start and end of request
318 2
        static $identifier_cache = [];
319 2
        if (isset($identifier_cache[$context])) {
320 1
            return $identifier_cache[$context];
321
        }
322
323 2
        $identifier_source = '';
324
325 2
        $cache_strategy = $this->config->get('cache_module_content_caching_strategy');
326
327 2
        switch ($cache_strategy) {
328 2
            case 'memberships':
329
                if (!midcom::get()->auth->is_valid_user()) {
330
                    $identifier_source .= 'USER=ANONYMOUS';
331
                    break;
332
                }
333
334
                $mc = new midgard_collector('midgard_member', 'uid', midcom_connection::get_user());
335
                $mc->set_key_property('gid');
336
                $mc->execute();
337
                $gids = $mc->list_keys();
338
                $identifier_source .= 'GROUPS=' . implode(',', array_keys($gids));
339
                break;
340 2
            case 'public':
341
                $identifier_source .= 'USER=EVERYONE';
342
                break;
343 2
            case 'user':
344
            default:
345 2
                $identifier_source .= 'USER=' . midcom_connection::get_user();
346 2
                break;
347
        }
348
349 2
        $identifier_source .= ';URL=' . $request->getUri();
350 2
        debug_add("Generating context {$context} request-identifier from: {$identifier_source}");
351
352 2
        $identifier_cache[$context] = 'R-' . md5($identifier_source);
353 2
        return $identifier_cache[$context];
354
    }
355
356 2
    private function get_strategy(string $name) : string
357
    {
358 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

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