Completed
Push — master ( 1f5a3f...9eae3e )
by Andreas
24:45 queued 10:44
created

midcom_services_cache_module_content::_check_hit()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 60
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 14.0601

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

718
                $response->setLastModified(/** @scrutinizer ignore-type */ new DateTimeImmutable);
Loading history...
719
            }
720
        } else {
721
            /* Determine Last-Modified using MidCOM's component context,
722
             * Fallback to time() if this fails.
723
             */
724
            $time = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_LASTMODIFIED) ?: time();
725 1
            $response->setLastModified(DateTimeImmutable::createFromFormat('U', (string) $time));
0 ignored issues
show
Bug introduced by
DateTimeImmutable::creat...mat('U', (string)$time) of type DateTimeImmutable|false is incompatible with the type DateTime|null expected by parameter $date of Symfony\Component\HttpFo...onse::setLastModified(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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

773
                ->setExpires(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $expires))
Loading history...
774 1
                ->setMaxAge($max_age);
775
            if ($max_age == 0) {
776
                $response->headers->addCacheControlDirective('must-revalidate');
777
            }
778
        }
779 1
    }
780
}
781