Passed
Push — master ( ae9760...35700c )
by Andreas
17:19
created

midcom_services_cache_module_content::expires()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

707
            $response->setLastModified(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', (string) $time));
Loading history...
708
        }
709
710 1
        if (!$response->headers->has('Content-Length')) {
711
            /* TODO: Doublecheck the way this is handled, now we just don't send it
712
             * if headers_strategy implies caching */
713 1
            if (!in_array($this->_headers_strategy, ['public', 'private'])) {
714 1
                $response->headers->set("Content-Length", strlen($response->getContent()));
715
            }
716
        }
717
718 1
        $this->cache_control_headers($response);
719 1
    }
720
721
    /**
722
     * @param Response $response
723
     */
724 1
    public function cache_control_headers(Response $response)
725
    {
726
        // Just to be sure not to mess the headers sent by no_cache in case it was called
727 1
        if ($this->_no_cache) {
728
            $this->no_cache($response);
729
        } else {
730
            // Add Expiration and Cache Control headers
731 1
            $strategy = $this->_headers_strategy;
732 1
            $default_lifetime = $this->_default_lifetime;
733 1
            if (   midcom::get()->auth->is_valid_user()
734 1
                || midcom_connection::get_user()) {
735
                $strategy = $this->_headers_strategy_authenticated;
736
                $default_lifetime = $this->_default_lifetime_authenticated;
737
            }
738
739 1
            $now = time();
740 1
            if ($strategy == 'revalidate') {
741
                // If expires is not set, we force the client to revalidate every time.
742
                // The timeout of a content cache entry is not affected by this.
743 1
                $expires = $this->_expires ?? $now;
744
            } else {
745
                $expires = $this->_expires ?? $now + $default_lifetime;
746
                if ($strategy == 'private') {
747
                    $response->setPrivate();
748
                } else {
749
                    $response->setPublic();
750
                }
751
            }
752 1
            $max_age = $expires - $now;
753
754
            $response
755 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

755
                ->setExpires(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $expires))
Loading history...
756 1
                ->setMaxAge($max_age);
757 1
            if ($max_age == 0) {
758 1
                $response->headers->addCacheControlDirective('must-revalidate');
759
            }
760
        }
761 1
    }
762
}
763