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

midcom_services_cache_module_content::_check_hit()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 59
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 15.123

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 34
c 1
b 0
f 0
nc 17
nop 1
dl 0
loc 59
ccs 22
cts 35
cp 0.6286
crap 15.123
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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