Passed
Push — master ( b22f9f...a1c8c9 )
by Andreas
09:45
created

midcom_services_cache_module_content   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 595
Duplicated Lines 0 %

Test Coverage

Coverage 73.31%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 233
c 2
b 0
f 0
dl 0
loc 595
ccs 173
cts 236
cp 0.7331
rs 2.56
wmc 73

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 2
A write_meta_cache() 0 17 3
A no_cache() 0 15 4
A content_type() 0 3 1
A check_dl_hit() 0 12 3
B _check_hit() 0 47 7
A enable_live_mode() 0 4 1
A complete_sent_headers() 0 18 5
A store_dl_content() 0 21 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 51 9
A register() 0 17 3
A on_request() 0 12 4
B generate_request_identifier() 0 40 6
A invalidate() 0 11 3
A register_sent_header() 0 5 2
B store_context_guid_map() 0 29 7

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

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