Completed
Push — master ( e91e95...8e3f70 )
by Andreas
17:11
created

midcom_services_cache_module_content::on_request()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 3
rs 10
c 1
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 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
     * Page expiration in seconds. If null (unset), the page does
76
     * not expire.
77
     *
78
     * @var int
79
     */
80
    private $_expires;
81
82
    /**
83
     * An array storing all HTTP headers registered through register_sent_header().
84
     * They will be sent when a cached page is delivered.
85
     *
86
     * @var array
87
     */
88
    private $_sent_headers = [];
89
90
    /**
91
     * Set this to true if you want to inhibit storage of the generated pages in
92
     * the cache database. All other headers will be created as usual though, so
93
     * 304 processing will kick in for example.
94
     *
95
     * @var boolean
96
     */
97
    private $_uncached = false;
98
99
    /**
100
     * Controls cache headers strategy
101
     * 'no-cache' activates no-cache mode that actively tries to circumvent all caching
102
     * '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
103
     * 'public' and 'private' enable caching with the cache-control header of the same name, default expiry timestamps are generated using the default_lifetime
104
     *
105
     * @var string
106
     */
107
    private $_headers_strategy = 'revalidate';
108
109
    /**
110
     * Controls cache headers strategy for authenticated users, needed because some proxies store cookies, too,
111
     * making a horrible mess when used by mix of authenticated and non-authenticated users
112
     *
113
     * @see $_headers_strategy
114
     * @var string
115
     */
116
    private $_headers_strategy_authenticated = 'private';
117
118
    /**
119
     * Default lifetime of page for public/private headers strategy
120
     * When generating the default expires header this is added to time().
121
     *
122
     * @var int
123
     */
124
    private $_default_lifetime = 0;
125
126
    /**
127
     * Default lifetime of page for public/private headers strategy for authenticated users
128
     *
129
     * @see $_default_lifetime
130
     * @var int
131
     */
132
    private $_default_lifetime_authenticated = 0;
133
134
    /**
135
     * A cache backend used to store the actual cached pages.
136
     *
137
     * @var Doctrine\Common\Cache\CacheProvider
138
     */
139
    private $_data_cache;
140
141
    /**
142
     * GUIDs loaded per context in this request
143
     */
144
    private $context_guids = [];
145
146
    /**
147
     * @var midcom_config
148
     */
149
    private $config;
150
151
    /**
152
     * Initialize the cache.
153
     *
154
     * The first step is to initialize the cache backends. The names of the
155
     * cache backends used for meta and data storage are derived from the name
156
     * defined for this module (see the 'name' configuration parameter above).
157
     * The name is used directly for the meta data cache, while the actual data
158
     * is stored in a backend postfixed with '_data'.
159
     *
160
     * After core initialization, the module checks for a cache hit (which might
161
     * trigger the delivery of the cached page and exit) and start the output buffer
162
     * afterwards.
163
     */
164 1
    public function __construct(midcom_config $config, CacheProvider $backend, CacheProvider $data_cache)
165
    {
166 1
        parent::__construct($backend);
167 1
        $this->config = $config;
168 1
        $this->_data_cache = $data_cache;
169 1
        $this->_data_cache->setNamespace($backend->getNamespace());
170
171 1
        $this->_uncached = $config->get('cache_module_content_uncached');
172 1
        $this->_headers_strategy = $this->get_strategy('cache_module_content_headers_strategy');
173 1
        $this->_headers_strategy_authenticated = $this->get_strategy('cache_module_content_headers_strategy_authenticated');
174 1
        $this->_default_lifetime = (int)$config->get('cache_module_content_default_lifetime');
175 1
        $this->_default_lifetime_authenticated = (int)$config->get('cache_module_content_default_lifetime_authenticated');
176
177 1
        if ($this->_headers_strategy == 'no-cache') {
178
            // we can't call no_cache() here, because it would try to call back to this class via the global getter
179
            $header = 'Cache-Control: no-store, no-cache, must-revalidate';
180
            $this->register_sent_header($header);
181
            midcom_compat_environment::get()->header($header);
182
            $this->_no_cache = true;
183
        }
184 1
    }
185
186
    /**
187
     * @param RequestEvent $event
188
     */
189 338
    public function on_request(RequestEvent $event)
190
    {
191 338
        if ($event->isMasterRequest()) {
192 1
            $request = $event->getRequest();
193
            /* Load and start up the cache system, this might already end the request
194
             * on a content cache hit. Note that the cache check hit depends on the i18n and auth code.
195
             */
196 1
            if ($response = $this->_check_hit($request)) {
197 1
                $event->setResponse($response);
198
            }
199
        }
200 338
    }
201
202
    /**
203
     * This function holds the cache hit check mechanism. It searches the requested
204
     * URL in the cache database. If found, it checks, whether the cache page has
205
     * expired. If not, the response is returned. In all other cases this method simply
206
     * returns void.
207
     *
208
     * Also, any HTTP POST request will automatically circumvent the cache so that
209
     * any component can process the request. It will set no_cache automatically
210
     * to avoid any cache pages being overwritten by, for example, search results.
211
     *
212
     * Note, that HTTP GET is <b>not</b> checked this way, as GET requests can be
213
     * safely distinguished by their URL.
214
     *
215
     * @param Request $request The request object
216
     * @return void|Response
217
     */
218 1
    private function _check_hit(Request $request)
219
    {
220 1
        if (!$request->isMethodCacheable()) {
221
            debug_add('Request method is not cacheable, setting no_cache');
222
            $this->no_cache();
223
            return;
224
        }
225
226
        // Check for uncached operation
227 1
        if ($this->_uncached) {
228
            debug_add("Uncached mode");
229
            return;
230
        }
231
232
        // Check that we have cache for the identifier
233 1
        $request_id = $this->generate_request_identifier($request);
234
        // Load metadata for the content identifier connected to current request
235 1
        $content_id = $this->backend->fetch($request_id);
236 1
        if ($content_id === false) {
237 1
            debug_add("MISS {$request_id}");
238
            // We have no information about content cached for this request
239 1
            return;
240
        }
241 1
        debug_add("HIT {$request_id}");
242
243 1
        $headers = $this->backend->fetch($content_id);
244 1
        if ($headers === false) {
245
            debug_add("MISS meta_cache {$content_id}");
246
            // Content cache data is missing
247
            return;
248
        }
249
250 1
        debug_add("HIT {$content_id}");
251
252 1
        $response = new Response('', Response::HTTP_OK, $headers);
253 1
        if (!$response->isNotModified($request)) {
254 1
            $content = $this->_data_cache->fetch($content_id);
255 1
            if ($content === false) {
256
                debug_add("Current page is in not in the data cache, possible ghost read.", MIDCOM_LOG_WARN);
257
                return;
258
            }
259 1
            $response->setContent($content);
260
        }
261
        // disable cache writing in on_response
262 1
        $this->_no_cache = true;
263 1
        return $response;
264
    }
265
266
    /**
267
     * This completes the output caching, post-processes it and updates the cache databases accordingly.
268
     *
269
     * The first step is to check against _no_cache pages, which will be delivered immediately
270
     * without any further post processing. Afterwards, the system will complete the sent
271
     * headers by adding all missing headers. Note, that E-Tag will be generated always
272
     * automatically, you must not set this in your component.
273
     *
274
     * If the midcom configuration option cache_uncached is set or the corresponding runtime function
275
     * has been called, the cache file will not be written, but the header stuff will be added like
276
     * usual to allow for browser-side caching.
277
     *
278
     * @param ResponseEvent $event The request object
279
     */
280 339
    public function on_response(ResponseEvent $event)
281
    {
282 339
        if (!$event->isMasterRequest()) {
283 338
            return;
284
        }
285 1
        $response = $event->getResponse();
286 1
        if ($response instanceof BinaryFileResponse) {
287
            return;
288
        }
289 1
        foreach ($this->_sent_headers as $header => $value) {
290
            // This can happen in streamed responses which enable_live_mode
291
            if (!headers_sent()) {
292
                header_remove($header);
293
            }
294
            $response->headers->set($header, $value);
295
        }
296 1
        $request = $event->getRequest();
297 1
        if ($this->_no_cache) {
298
            $response->prepare($request);
299
            return;
300
        }
301
302 1
        $cache_data = $response->getContent();
303
304
        // Register additional Headers around the current output request
305 1
        $this->complete_sent_headers($response);
306 1
        $response->prepare($request);
307
308
        // Generate E-Tag header.
309 1
        if (empty($cache_data)) {
310
            $etag = md5(serialize($response->headers->all()));
311
        } else {
312 1
            $etag = md5($cache_data);
313
        }
314 1
        $response->setEtag($etag);
315
316 1
        if ($this->_uncached) {
317
            debug_add('Not writing cache file, we are in uncached operation mode.');
318
            return;
319
        }
320 1
        $content_id = 'C-' . $etag;
321 1
        $this->write_meta_cache($content_id, $request, $response);
322 1
        $this->_data_cache->save($content_id, $cache_data);
323 1
    }
324
325
    /**
326
     * Generate a valid cache identifier for a context of the current request
327
     */
328 1
    private function generate_request_identifier(Request $request) : string
329
    {
330 1
        $context = $request->attributes->get('context')->id;
331
        // Cache the request identifier so that it doesn't change between start and end of request
332 1
        static $identifier_cache = [];
333 1
        if (isset($identifier_cache[$context])) {
334 1
            return $identifier_cache[$context];
335
        }
336
337 1
        $module_name = $this->config->get('cache_module_content_name');
338 1
        if ($module_name == 'auto') {
339 1
            $module_name = midcom_connection::get_unique_host_name();
340
        }
341 1
        $identifier_source = 'CACHE:' . $module_name;
342
343 1
        $cache_strategy = $this->config->get('cache_module_content_caching_strategy');
344
345 1
        switch ($cache_strategy) {
346 1
            case 'memberships':
347
                if (!midcom_connection::get_user()) {
348
                    $identifier_source .= ';USER=ANONYMOUS';
349
                    break;
350
                }
351
                $mc = new midgard_collector('midgard_member', 'uid', midcom_connection::get_user());
352
                $mc->set_key_property('gid');
353
                $mc->execute();
354
                $gids = $mc->list_keys();
355
                $identifier_source .= ';GROUPS=' . implode(',', array_keys($gids));
356
                break;
357 1
            case 'public':
358
                $identifier_source .= ';USER=EVERYONE';
359
                break;
360 1
            case 'user':
361
            default:
362 1
                $identifier_source .= ';USER=' . midcom_connection::get_user();
363 1
                break;
364
        }
365
366 1
        $identifier_source .= ';URL=' . $request->getRequestUri();
367 1
        debug_add("Generating context {$context} request-identifier from: {$identifier_source}");
368
369 1
        $identifier_cache[$context] = 'R-' . md5($identifier_source);
370 1
        return $identifier_cache[$context];
371
    }
372
373 1
    private function get_strategy(string $name) : string
374
    {
375 1
        $strategy = strtolower($this->config->get($name));
376 1
        $allowed = ['no-cache', 'revalidate', 'public', 'private'];
377 1
        if (!in_array($strategy, $allowed)) {
378
            throw new midcom_error($name . ' is not valid, try ' . implode(', ', $allowed));
379
        }
380 1
        return $strategy;
381
    }
382
383
    /**
384
     * Call this, if the currently processed output must not be cached for any
385
     * reason. Dynamic pages with sensitive content are a candidate for this
386
     * function.
387
     *
388
     * Note, that this will prevent <i>any</i> content invalidation related headers
389
     * like E-Tag to be generated automatically, and that the appropriate
390
     * no-store/no-cache headers from HTTP 1.1 and HTTP 1.0 will be sent automatically.
391
     * This means that there will also be no 304 processing.
392
     *
393
     * You should use this only for sensitive content. For simple dynamic output,
394
     * you are strongly encouraged to use the less strict uncached() function.
395
     *
396
     * @see uncached()
397
     */
398 191
    public function no_cache(Response $response = null)
399
    {
400 191
        $settings = 'no-store, no-cache, must-revalidate';
401
        // PONDER: Send expires header (set to long time in past) as well ??
402
403 191
        if ($response) {
404
            $response->headers->set('Cache-Control', $settings);
405 191
        } elseif (!$this->_no_cache) {
406
            if (headers_sent()) {
407
                debug_add('Warning, we should move to no_cache but headers have already been sent, skipping header transmission.', MIDCOM_LOG_ERROR);
408
            } else {
409
                midcom::get()->header('Cache-Control: ' . $settings);
410
            }
411
        }
412 191
        $this->_no_cache = true;
413 191
    }
414
415
    /**
416
     * Call this, if the currently processed output must not be cached for any
417
     * reason. Dynamic pages or form processing results are the usual candidates
418
     * for this mode.
419
     *
420
     * Note, that this will still keep the caching engine active so that it can
421
     * add the usual headers (ETag, Expires ...) in respect to the no_cache flag.
422
     * As well, at the end of the processing, the usual 304 checks are done, so if
423
     * your page doesn't change in respect of E-Tag and Last-Modified, only a 304
424
     * Not Modified reaches the client.
425
     *
426
     * Essentially, no_cache behaves the same way as if the uncached configuration
427
     * directive is set to true, it is just limited to a single request.
428
     *
429
     * If you need a higher level of client side security, to avoid storage of sensitive
430
     * information on the client side, you should use no_cache instead.
431
     *
432
     * @see no_cache()
433
     */
434 3
    public function uncached(bool $uncached = true)
435
    {
436 3
        $this->_uncached = $uncached;
437 3
    }
438
439
    /**
440
     * Sets the expiration time of the current page (Unix (GMT) Timestamp).
441
     *
442
     * <b>Note:</B> This generate error call will add browser-side cache control
443
     * headers as well to force a browser to revalidate a page after the set
444
     * expiry.
445
     *
446
     * You should call this at all places where you have timed content in your
447
     * output, so that the page will be regenerated once a certain article has
448
     * expired.
449
     *
450
     * Multiple calls to expires will only save the
451
     * "youngest" timestamp, so you can safely call expires where appropriate
452
     * without respect to other values.
453
     *
454
     * The cache's default (null) will disable the expires header. Note, that once
455
     * an expiry time on a page has been set, it is not possible, to reset it again,
456
     * this is for dynamic_load situation, where one component might depend on a
457
     * set expiry.
458
     *
459
     * @param int $timestamp The UNIX timestamp from which the cached page should be invalidated.
460
     */
461
    public function expires($timestamp)
462
    {
463
        if (   $this->_expires === null
464
            || $this->_expires > $timestamp) {
465
            $this->_expires = $timestamp;
466
        }
467
    }
468
469
    /**
470
     * Sets the content type for the current page. The required HTTP Headers for
471
     * are automatically generated, so, to the contrary of expires, you just have
472
     * to set this header accordingly.
473
     *
474
     * This is usually set automatically by MidCOM for all regular HTML output and
475
     * for all attachment deliveries. You have to adapt it only for things like RSS
476
     * output.
477
     *
478
     * @param string $type    The content type to use.
479
     */
480 8
    public function content_type($type)
481
    {
482 8
        midcom::get()->header('Content-Type: ' . $type);
483 8
    }
484
485
    /**
486
     * Put the cache into a "live mode". This will disable the
487
     * cache during runtime, correctly flushing the output buffer (if it's not empty)
488
     * and sending cache control headers.
489
     *
490
     * The midcom-exec URL handler of the core will automatically enable live mode.
491
     *
492
     * @see midcom_application::_exec_file()
493
     */
494
    public function enable_live_mode()
495
    {
496
        $this->no_cache();
497
        Response::closeOutputBuffers(0, ob_get_length() > 0);
498
    }
499
500
    /**
501
     * Store a sent header into the cache database, so that it will
502
     * be resent when the cache page is delivered. midcom_application::header()
503
     * will automatically call this function, you need to do this only if you use
504
     * the PHP header function.
505
     *
506
     * @param string $header The header that was sent.
507
     */
508 17
    public function register_sent_header($header)
509
    {
510 17
        if (str_contains($header, ': ')) {
511 17
            [$header, $value] = explode(': ', $header, 2);
512 17
            $this->_sent_headers[$header] = $value;
513
        }
514 17
    }
515
516
    /**
517
     * Looks for list of content and request identifiers paired with the given guid
518
     * and removes all of those from the caches.
519
     *
520
     * {@inheritDoc}
521
     */
522 289
    public function invalidate($guid, $object = null)
523
    {
524 289
        $guidmap = $this->backend->fetch($guid);
525 289
        if ($guidmap === false) {
526 289
            debug_add("No entry for {$guid} in meta cache, ignoring invalidation request.");
527 289
            return;
528
        }
529
530
        foreach ($guidmap as $content_id) {
531
            if ($this->backend->contains($content_id)) {
532
                $this->backend->delete($content_id);
533
            }
534
535
            if ($this->_data_cache->contains($content_id)) {
536
                $this->_data_cache->delete($content_id);
537
            }
538
        }
539
    }
540
541
    public function invalidate_all()
542
    {
543
        parent::invalidate_all();
544
        $this->_data_cache->flushAll();
545
    }
546
547
    /**
548
     * All objects loaded within a request are stored into a list for cache invalidation purposes
549
     */
550 393
    public function register($guid)
551
    {
552
        // Check for uncached operation
553 393
        if ($this->_uncached) {
554 393
            return;
555
        }
556
557
        $context = midcom_core_context::get()->id;
558
        if ($context != 0) {
559
            // We're in a dynamic_load, register it for that as well
560
            if (!isset($this->context_guids[$context])) {
561
                $this->context_guids[$context] = [];
562
            }
563
            $this->context_guids[$context][] = $guid;
564
        }
565
566
        // Register all GUIDs also to the root context
567
        if (!isset($this->context_guids[0])) {
568
            $this->context_guids[0] = [];
569
        }
570
        $this->context_guids[0][] = $guid;
571
    }
572
573
    /**
574
     * Writes meta-cache entry from context data using given content id
575
     * Used to be part of on_request, but needed by serve-attachment method in midcom_core_urlmethods as well
576
     */
577 1
    public function write_meta_cache($content_id, Request $request, Response $response)
578
    {
579 1
        if (   $this->_uncached
580 1
            || $this->_no_cache) {
581
            return;
582
        }
583
584 1
        if ($this->_expires !== null) {
585
            $lifetime = $this->_expires - time();
586
        } else {
587
            // Use default expiry for cache entry, most components don't bother calling expires() properly
588 1
            $lifetime = $this->_default_lifetime;
589
        }
590
591
        // Construct cache identifier
592 1
        $request_id = $this->generate_request_identifier($request);
593
594
        $entries = [
595 1
            $request_id => $content_id,
596 1
            $content_id => $response->headers->all()
597
        ];
598 1
        $this->backend->saveMultiple($entries, $lifetime);
599
600
        // Cache where the object have been
601 1
        $context = midcom_core_context::get()->id;
602 1
        $this->store_context_guid_map($context, $content_id, $request_id);
603 1
    }
604
605 1
    private function store_context_guid_map($context, string $content_id, string $request_id)
606
    {
607
        // non-existent context
608 1
        if (!array_key_exists($context, $this->context_guids)) {
609 1
            return;
610
        }
611
612
        $maps = $this->backend->fetchMultiple($this->context_guids[$context]);
613
        $to_save = [];
614
        foreach ($this->context_guids[$context] as $guid) {
615
            // Getting old map from cache or create new, empty one
616
            $guidmap = $maps[$guid] ?? [];
617
618
            if (!in_array($content_id, $guidmap)) {
619
                $guidmap[] = $content_id;
620
                $to_save[$guid] = $guidmap;
621
            }
622
623
            if (!in_array($request_id, $guidmap)) {
624
                $guidmap[] = $request_id;
625
                $to_save[$guid] = $guidmap;
626
            }
627
        }
628
629
        $this->backend->saveMultiple($to_save);
630
    }
631
632 16
    public function check_dl_hit(Request $request)
633
    {
634 16
        if ($this->_no_cache) {
635 16
            return false;
636
        }
637
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
638
        $dl_content_id = $this->backend->fetch($dl_request_id);
639
        if ($dl_content_id === false) {
640
            return false;
641
        }
642
643
        return $this->_data_cache->fetch($dl_content_id);
644
    }
645
646 4
    public function store_dl_content($context, $dl_cache_data, Request $request)
647
    {
648 4
        if (   $this->_no_cache
649 4
            || $this->_uncached) {
650 4
            return;
651
        }
652
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
653
        $dl_content_id = 'DLC-' . md5($dl_cache_data);
654
655
        if ($this->_expires !== null) {
656
            $lifetime = $this->_expires - time();
657
        } else {
658
            // Use default expiry for cache entry, most components don't bother calling expires() properly
659
            $lifetime = $this->_default_lifetime;
660
        }
661
        $this->backend->save($dl_request_id, $dl_content_id, $lifetime);
662
        $this->_data_cache->save($dl_content_id, $dl_cache_data, $lifetime);
663
        // Cache where the object have been
664
        $this->store_context_guid_map($context, $dl_content_id, $dl_request_id);
665
    }
666
667
    /**
668
     * This little helper ensures that the headers Content-Length
669
     * and Last-Modified are present. The lastmod timestamp is taken out of the
670
     * component context information if it is populated correctly there; if not, the
671
     * system time is used instead.
672
     *
673
     * To force browsers to revalidate the page on every request (login changes would
674
     * go unnoticed otherwise), the Cache-Control header max-age=0 is added automatically.
675
     */
676 1
    private function complete_sent_headers(Response $response)
677
    {
678 1
        if (!$response->getLastModified()) {
679
            /* Determine Last-Modified using MidCOM's component context,
680
             * Fallback to time() if this fails.
681
             */
682 1
            $time = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_LASTMODIFIED) ?: time();
683 1
            $response->setLastModified(DateTime::createFromFormat('U', (string) $time));
0 ignored issues
show
Bug introduced by
It seems like DateTime::createFromFormat('U', (string)$time) can also be of type false; however, parameter $date of Symfony\Component\HttpFo...onse::setLastModified() does only seem to accept DateTimeInterface|null, 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

683
            $response->setLastModified(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', (string) $time));
Loading history...
684
        }
685
686 1
        if (!$response->headers->has('Content-Length')) {
687
            /* TODO: Doublecheck the way this is handled, now we just don't send it
688
             * if headers_strategy implies caching */
689 1
            if (!in_array($this->_headers_strategy, ['public', 'private'])) {
690 1
                $response->headers->set("Content-Length", strlen($response->getContent()));
691
            }
692
        }
693
694 1
        $this->cache_control_headers($response);
695 1
    }
696
697
    /**
698
     * @param Response $response
699
     */
700 1
    public function cache_control_headers(Response $response)
701
    {
702
        // Just to be sure not to mess the headers sent by no_cache in case it was called
703 1
        if ($this->_no_cache) {
704
            $this->no_cache($response);
705
        } else {
706
            // Add Expiration and Cache Control headers
707 1
            $strategy = $this->_headers_strategy;
708 1
            $default_lifetime = $this->_default_lifetime;
709 1
            if (   midcom::get()->auth->is_valid_user()
710 1
                || midcom_connection::get_user()) {
711
                $strategy = $this->_headers_strategy_authenticated;
712
                $default_lifetime = $this->_default_lifetime_authenticated;
713
            }
714
715 1
            $now = time();
716 1
            if ($strategy == 'revalidate') {
717
                // If expires is not set, we force the client to revalidate every time.
718
                // The timeout of a content cache entry is not affected by this.
719 1
                $expires = $this->_expires ?? $now;
720
            } else {
721
                $expires = $this->_expires ?? $now + $default_lifetime;
722
                if ($strategy == 'private') {
723
                    $response->setPrivate();
724
                } else {
725
                    $response->setPublic();
726
                }
727
            }
728 1
            $max_age = $expires - $now;
729
730
            $response
731 1
                ->setExpires(DateTime::createFromFormat('U', $expires))
0 ignored issues
show
Bug introduced by
It seems like DateTime::createFromFormat('U', $expires) can also be of type false; however, parameter $date of Symfony\Component\HttpFo...\Response::setExpires() does only seem to accept DateTimeInterface|null, 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

731
                ->setExpires(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $expires))
Loading history...
732 1
                ->setMaxAge($max_age);
733 1
            if ($max_age == 0) {
734 1
                $response->headers->addCacheControlDirective('must-revalidate');
735
            }
736
        }
737 1
    }
738
}
739