Passed
Push — master ( be6fbd...8a140a )
by Andreas
24:16
created

midcom_services_cache_module_content   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 604
Duplicated Lines 0 %

Test Coverage

Coverage 61.51%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 222
dl 0
loc 604
ccs 147
cts 239
cp 0.6151
rs 2.48
c 4
b 0
f 0
wmc 74

20 Methods

Rating   Name   Duplication   Size   Complexity  
A write_meta_cache() 0 19 3
A no_cache() 0 15 4
A content_type() 0 3 1
A check_dl_hit() 0 12 3
B _check_hit() 0 46 7
A enable_live_mode() 0 4 1
A complete_sent_headers() 0 18 5
A store_dl_content() 0 13 3
B cache_control_headers() 0 30 6
A uncached() 0 3 1
A get_strategy() 0 8 2
A invalidate_all() 0 4 1
B on_response() 0 43 8
A register() 0 21 5
A on_request() 0 9 3
A __construct() 0 19 2
B generate_request_identifier() 0 44 7
A invalidate() 0 15 5
A register_sent_header() 0 5 2
A store_context_guid_map() 0 25 5

How to fix   Complexity   

Complex Class

Complex classes like midcom_services_cache_module_content 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 midcom_services_cache_module_content, and based on these observations, apply Extract Interface, too.

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 Doctrine\Common\Cache\CacheProvider;
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>string cache_module_content_name</i>: The name of the cache database to use. This should usually be tied to the actual
56
 *   MidCOM site to have exactly one cache per site. This is mandatory (and populated by a sensible default
57
 *   by midcom_config, see there for details).
58
 * - <i>boolean cache_module_content_uncached</i>: Set this to true to prevent the saving of cached pages. This is useful
59
 *   for development work, as all other headers (like E-Tag or Last-Modified) are generated
60
 *   normally. See the uncached() and _uncached members.
61
 *
62
 * @package midcom.services
63
 */
64
class midcom_services_cache_module_content extends midcom_services_cache_module
65
{
66
    /**
67
     * Flag, indicating whether the current page may be cached. If
68
     * false, the usual no-cache headers will be generated.
69
     *
70
     * @var boolean
71
     */
72
    private $_no_cache = false;
73
74
    /**
75
     * An array storing all HTTP headers registered through register_sent_header().
76
     * They will be sent when a cached page is delivered.
77
     *
78
     * @var array
79
     */
80
    private $_sent_headers = [];
81
82
    /**
83
     * Set this to true if you want to inhibit storage of the generated pages in
84
     * the cache database. All other headers will be created as usual though, so
85
     * 304 processing will kick in for example.
86
     *
87
     * @var boolean
88
     */
89
    private $_uncached = false;
90
91
    /**
92
     * Controls cache headers strategy
93
     * 'no-cache' activates no-cache mode that actively tries to circumvent all caching
94
     * '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
95
     * 'public' and 'private' enable caching with the cache-control header of the same name, default expiry timestamps are generated using the default_lifetime
96
     *
97
     * @var string
98
     */
99
    private $_headers_strategy = 'revalidate';
100
101
    /**
102
     * Controls cache headers strategy for authenticated users, needed because some proxies store cookies, too,
103
     * making a horrible mess when used by mix of authenticated and non-authenticated users
104
     *
105
     * @see $_headers_strategy
106
     * @var string
107
     */
108
    private $_headers_strategy_authenticated = 'private';
109
110
    /**
111
     * Default lifetime of page for public/private headers strategy
112
     * When generating the default expires header this is added to time().
113
     *
114
     * @var int
115
     */
116
    private $_default_lifetime = 0;
117
118
    /**
119
     * Default lifetime of page for public/private headers strategy for authenticated users
120
     *
121
     * @see $_default_lifetime
122
     * @var int
123
     */
124
    private $_default_lifetime_authenticated = 0;
125
126
    /**
127
     * A cache backend used to store the actual cached pages.
128
     *
129
     * @var Doctrine\Common\Cache\CacheProvider
130
     */
131
    private $_data_cache;
132
133
    /**
134
     * GUIDs loaded per context in this request
135
     */
136
    private $context_guids = [];
137
138
    /**
139
     * @var midcom_config
140
     */
141
    private $config;
142
143
    /**
144
     * Initialize the cache.
145
     *
146
     * The first step is to initialize the cache backends. The names of the
147
     * cache backends used for meta and data storage are derived from the name
148
     * defined for this module (see the 'name' configuration parameter above).
149
     * The name is used directly for the meta data cache, while the actual data
150
     * is stored in a backend postfixed with '_data'.
151
     *
152
     * After core initialization, the module checks for a cache hit (which might
153
     * trigger the delivery of the cached page and exit) and start the output buffer
154
     * afterwards.
155
     */
156 1
    public function __construct(midcom_config $config, CacheProvider $backend, CacheProvider $data_cache)
157
    {
158 1
        parent::__construct($backend);
159 1
        $this->config = $config;
160 1
        $this->_data_cache = $data_cache;
161 1
        $this->_data_cache->setNamespace($backend->getNamespace());
162
163 1
        $this->_uncached = $config->get('cache_module_content_uncached');
164 1
        $this->_headers_strategy = $this->get_strategy('cache_module_content_headers_strategy');
165 1
        $this->_headers_strategy_authenticated = $this->get_strategy('cache_module_content_headers_strategy_authenticated');
166 1
        $this->_default_lifetime = (int)$config->get('cache_module_content_default_lifetime');
167 1
        $this->_default_lifetime_authenticated = (int)$config->get('cache_module_content_default_lifetime_authenticated');
168
169 1
        if ($this->_headers_strategy == 'no-cache') {
170
            // we can't call no_cache() here, because it would try to call back to this class via the global getter
171
            $header = 'Cache-Control: no-store, no-cache, must-revalidate';
172
            $this->register_sent_header($header);
173
            midcom_compat_environment::header($header);
174
            $this->_no_cache = true;
175
        }
176 1
    }
177
178 342
    public function on_request(RequestEvent $event)
179
    {
180 342
        if ($event->isMasterRequest()) {
181 1
            $request = $event->getRequest();
182
            /* Load and start up the cache system, this might already end the request
183
             * on a content cache hit. Note that the cache check hit depends on the i18n and auth code.
184
             */
185 1
            if ($response = $this->_check_hit($request)) {
186 1
                $event->setResponse($response);
187
            }
188
        }
189 342
    }
190
191
    /**
192
     * This function holds the cache hit check mechanism. It searches the requested
193
     * URL in the cache database. If found, it checks, whether the cache page has
194
     * expired. If not, the response is returned. In all other cases this method simply
195
     * returns void.
196
     *
197
     * Also, any HTTP POST request will automatically circumvent the cache so that
198
     * any component can process the request. It will set no_cache automatically
199
     * to avoid any cache pages being overwritten by, for example, search results.
200
     *
201
     * Note, that HTTP GET is <b>not</b> checked this way, as GET requests can be
202
     * safely distinguished by their URL.
203
     *
204
     * @return void|Response
205
     */
206 1
    private function _check_hit(Request $request)
207
    {
208 1
        if (!$request->isMethodCacheable()) {
209
            debug_add('Request method is not cacheable, setting no_cache');
210
            $this->no_cache();
211
            return;
212
        }
213
214
        // Check for uncached operation
215 1
        if ($this->_uncached) {
216
            debug_add("Uncached mode");
217
            return;
218
        }
219
220
        // Check that we have cache for the identifier
221 1
        $request_id = $this->generate_request_identifier($request);
222
        // Load metadata for the content identifier connected to current request
223 1
        $content_id = $this->backend->fetch($request_id);
224 1
        if ($content_id === false) {
225 1
            debug_add("MISS {$request_id}");
226
            // We have no information about content cached for this request
227 1
            return;
228
        }
229 1
        debug_add("HIT {$request_id}");
230
231 1
        $headers = $this->backend->fetch($content_id);
232 1
        if ($headers === false) {
233
            debug_add("MISS meta_cache {$content_id}");
234
            // Content cache data is missing
235
            return;
236
        }
237
238 1
        debug_add("HIT {$content_id}");
239
240 1
        $response = new Response('', Response::HTTP_OK, $headers);
241 1
        if (!$response->isNotModified($request)) {
242 1
            $content = $this->_data_cache->fetch($content_id);
243 1
            if ($content === false) {
244
                debug_add("Current page is in not in the data cache, possible ghost read.", MIDCOM_LOG_WARN);
245
                return;
246
            }
247 1
            $response->setContent($content);
248
        }
249
        // disable cache writing in on_response
250 1
        $this->_no_cache = true;
251 1
        return $response;
252
    }
253
254
    /**
255
     * This completes the output caching, post-processes it and updates the cache databases accordingly.
256
     *
257
     * The first step is to check against _no_cache pages, which will be delivered immediately
258
     * without any further post processing. Afterwards, the system will complete the sent
259
     * headers by adding all missing headers. Note, that E-Tag will be generated always
260
     * automatically, you must not set this in your component.
261
     *
262
     * If the midcom configuration option cache_uncached is set or the corresponding runtime function
263
     * has been called, the cache file will not be written, but the header stuff will be added like
264
     * usual to allow for browser-side caching.
265
     *
266
     * @param ResponseEvent $event The request object
267
     */
268 343
    public function on_response(ResponseEvent $event)
269
    {
270 343
        if (!$event->isMasterRequest()) {
271 342
            return;
272
        }
273 1
        $response = $event->getResponse();
274 1
        if ($response instanceof BinaryFileResponse) {
275
            return;
276
        }
277 1
        foreach ($this->_sent_headers as $header => $value) {
278
            // This can happen in streamed responses which enable_live_mode
279
            if (!headers_sent()) {
280
                header_remove($header);
281
            }
282
            $response->headers->set($header, $value);
283
        }
284 1
        $request = $event->getRequest();
285 1
        if ($this->_no_cache) {
286
            $response->prepare($request);
287
            return;
288
        }
289
290 1
        $cache_data = $response->getContent();
291
292
        // Register additional Headers around the current output request
293 1
        $this->complete_sent_headers($response);
294 1
        $response->prepare($request);
295
296
        // Generate E-Tag header.
297 1
        if (empty($cache_data)) {
298
            $etag = md5(serialize($response->headers->all()));
299
        } else {
300 1
            $etag = md5($cache_data);
301
        }
302 1
        $response->setEtag($etag);
303
304 1
        if ($this->_uncached) {
305
            debug_add('Not writing cache file, we are in uncached operation mode.');
306
            return;
307
        }
308 1
        $content_id = 'C-' . $etag;
309 1
        $this->write_meta_cache($content_id, $request, $response);
310 1
        $this->_data_cache->save($content_id, $cache_data);
311 1
    }
312
313
    /**
314
     * Generate a valid cache identifier for a context of the current request
315
     */
316 1
    private function generate_request_identifier(Request $request) : string
317
    {
318 1
        $context = $request->attributes->get('context')->id;
319
        // Cache the request identifier so that it doesn't change between start and end of request
320 1
        static $identifier_cache = [];
321 1
        if (isset($identifier_cache[$context])) {
322 1
            return $identifier_cache[$context];
323
        }
324
325 1
        $module_name = $this->config->get('cache_module_content_name');
326 1
        if ($module_name == 'auto') {
327 1
            $module_name = midcom_connection::get_unique_host_name();
328
        }
329 1
        $identifier_source = 'CACHE:' . $module_name;
330
331 1
        $cache_strategy = $this->config->get('cache_module_content_caching_strategy');
332
333 1
        switch ($cache_strategy) {
334 1
            case 'memberships':
335
                if (!midcom::get()->auth->is_valid_user()) {
336
                    $identifier_source .= ';USER=ANONYMOUS';
337
                    break;
338
                }
339
340
                $mc = new midgard_collector('midgard_member', 'uid', midcom_connection::get_user());
341
                $mc->set_key_property('gid');
342
                $mc->execute();
343
                $gids = $mc->list_keys();
344
                $identifier_source .= ';GROUPS=' . implode(',', array_keys($gids));
345
                break;
346 1
            case 'public':
347
                $identifier_source .= ';USER=EVERYONE';
348
                break;
349 1
            case 'user':
350
            default:
351 1
                $identifier_source .= ';USER=' . midcom_connection::get_user();
352 1
                break;
353
        }
354
355 1
        $identifier_source .= ';URL=' . $request->getRequestUri();
356 1
        debug_add("Generating context {$context} request-identifier from: {$identifier_source}");
357
358 1
        $identifier_cache[$context] = 'R-' . md5($identifier_source);
359 1
        return $identifier_cache[$context];
360
    }
361
362 1
    private function get_strategy(string $name) : string
363
    {
364 1
        $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

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