Completed
Push — master ( df55da...114248 )
by Andreas
18:19
created

midcom_services_cache_module_content::_check_hit()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 62
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 13.1494

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 37
nc 17
nop 1
dl 0
loc 62
ccs 26
cts 38
cp 0.6842
crap 13.1494
rs 7.6666
c 1
b 0
f 0

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
     * The time of the last modification, set during auto-header-completion.
83
     *
84
     * @var int
85
     */
86
    private $_last_modified = 0;
87
88
    /**
89
     * An array storing all HTTP headers registered through register_sent_header().
90
     * They will be sent when a cached page is delivered.
91
     *
92
     * @var array
93
     */
94
    private $_sent_headers = [];
95
96
    /**
97
     * The MIME content-type of the current request. It defaults to text/html, but
98
     * must be set correctly, so that the client gets the correct type delivered
99
     * upon cache deliveries.
100
     *
101
     * @var string
102
     */
103
    private $_content_type = 'text/html';
104
105
    /**
106
     * Set this to true if you want to inhibit storage of the generated pages in
107
     * the cache database. All other headers will be created as usual though, so
108
     * 304 processing will kick in for example.
109
     *
110
     * @var boolean
111
     */
112
    private $_uncached = false;
113
114
    /**
115
     * Controls cache headers strategy
116
     * 'no-cache' activates no-cache mode that actively tries to circumvent all caching
117
     * '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
118
     * 'public' and 'private' enable caching with the cache-control header of the same name, default expiry timestamps are generated using the default_lifetime
119
     *
120
     * @var string
121
     */
122
    private $_headers_strategy = 'revalidate';
123
124
    /**
125
     * Controls cache headers strategy for authenticated users, needed because some proxies store cookies, too,
126
     * making a horrible mess when used by mix of authenticated and non-authenticated users
127
     *
128
     * @see $_headers_strategy
129
     * @var string
130
     */
131
    private $_headers_strategy_authenticated = 'private';
132
133
    /**
134
     * Default lifetime of page for public/private headers strategy
135
     * When generating the default expires header this is added to time().
136
     *
137
     * @var int
138
     */
139
    private $_default_lifetime = 0;
140
141
    /**
142
     * Default lifetime of page for public/private headers strategy for authenticated users
143
     *
144
     * @see $_default_lifetime
145
     * @var int
146
     */
147
    private $_default_lifetime_authenticated = 0;
148
149
    /**
150
     * Cache backend instance.
151
     *
152
     * @var Doctrine\Common\Cache\CacheProvider
153
     */
154
    private $_meta_cache;
155
156
    /**
157
     * A cache backend used to store the actual cached pages.
158
     *
159
     * @var Doctrine\Common\Cache\CacheProvider
160
     */
161
    private $_data_cache;
162
163
    /**
164
     * GUIDs loaded per context in this request
165
     */
166
    private $context_guids = [];
167
168
    /**
169
     * Forced headers
170
     */
171
    private $_force_headers = [];
172
173
    /**
174
     * @param GetResponseEvent $event
175
     */
176 338
    public function on_request(GetResponseEvent $event)
177
    {
178 338
        if ($event->isMasterRequest()) {
179 1
            $request = $event->getRequest();
180
            /* Load and start up the cache system, this might already end the request
181
             * on a content cache hit. Note that the cache check hit depends on the i18n and auth code.
182
             */
183 1
            if ($response = $this->_check_hit($request)) {
184 1
                $event->setResponse($response);
185
            }
186
        }
187 338
    }
188
189
    /**
190
     * This function holds the cache hit check mechanism. It searches the requested
191
     * URL in the cache database. If found, it checks, whether the cache page has
192
     * expired. If not, the response is returned. In all other cases this method simply
193
     * returns void.
194
     *
195
     * The midcom-cache URL methods are handled before checking for a cache hit.
196
     *
197
     * Also, any HTTP POST request will automatically circumvent the cache so that
198
     * any component can process the request. It will set no_cache automatically
199
     * to avoid any cache pages being overwritten by, for example, search results.
200
     *
201
     * Note, that HTTP GET is <b>not</b> checked this way, as GET requests can be
202
     * safely distinguished by their URL.
203
     *
204
     * @param Request $request The request object
205
     * @return void|Response
206
     */
207 1
    private function _check_hit(Request $request)
208
    {
209 1
        foreach (midcom_connection::get_url('argv') as $arg) {
210 1
            if (in_array($arg, ["midcom-cache-invalidate", "midcom-cache-nocache"])) {
211
                // Don't cache these.
212
                debug_add("uncached: $arg");
213 1
                return;
214
            }
215
        }
216
217 1
        if (!$request->isMethodCacheable()) {
218
            debug_add('Request method is not cacheable, setting no_cache');
219
            $this->no_cache();
220
            return;
221
        }
222
223
        // Check for uncached operation
224 1
        if ($this->_uncached) {
225
            debug_add("Uncached mode");
226
            return;
227
        }
228
229
        // Check that we have cache for the identifier
230 1
        $request_id = $this->generate_request_identifier($request);
231
        // Load metadata for the content identifier connected to current request
232 1
        $content_id = $this->_meta_cache->fetch($request_id);
233 1
        if ($content_id === false) {
234 1
            debug_add("MISS {$request_id}");
235
            // We have no information about content cached for this request
236 1
            return;
237
        }
238 1
        debug_add("HIT {$request_id}");
239
240 1
        $data = $this->_meta_cache->fetch($content_id);
241 1
        if ($data === false) {
242
            debug_add("MISS meta_cache {$content_id}");
243
            // Content cache data is missing
244
            return;
245
        }
246
247 1
        if (!isset($data['last_modified'])) {
248
            debug_add('Current page is in cache, but has insufficient information', MIDCOM_LOG_INFO);
249
            return;
250
        }
251
252 1
        debug_add("HIT {$content_id}");
253
254 1
        $response = new Response;
255 1
        $this->apply_headers($response, $data['sent_headers']);
256 1
        $response->setEtag($data['etag']);
257 1
        $response->setLastModified(DateTime::createFromFormat('U', $data['last_modified']));
0 ignored issues
show
Bug introduced by
It seems like DateTime::createFromForm...$data['last_modified']) can also be of type false; however, parameter $date of Symfony\Component\HttpFo...onse::setLastModified() 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

257
        $response->setLastModified(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $data['last_modified']));
Loading history...
258 1
        if (!$response->isNotModified($request)) {
259 1
            $content = $this->_data_cache->fetch($content_id);
260 1
            if ($content === false) {
261
                debug_add("Current page is in not in the data cache, possible ghost read.", MIDCOM_LOG_WARN);
262
                return;
263
            }
264 1
            $response->setContent($content);
265
        }
266
        // disable cache writing in on_response
267 1
        $this->_no_cache = true;
268 1
        return $response;
269
    }
270
271
    /**
272
     * This completes the output caching, post-processes it and updates the cache databases accordingly.
273
     *
274
     * The first step is to check against _no_cache pages, which will be delivered immediately
275
     * without any further post processing. Afterwards, the system will complete the sent
276
     * headers by adding all missing headers. Note, that E-Tag will be generated always
277
     * automatically, you must not set this in your component.
278
     *
279
     * If the midcom configuration option cache_uncached is set or the corresponding runtime function
280
     * has been called, the cache file will not be written, but the header stuff will be added like
281
     * usual to allow for browser-side caching.
282
     *
283
     * @param FilterResponseEvent $event The request object
284
     */
285 339
    public function on_response(FilterResponseEvent $event)
286
    {
287 339
        if ($this->_no_cache || !$event->isMasterRequest()) {
288 338
            return;
289
        }
290 1
        $response = $event->getResponse();
291 1
        if ($response instanceof BinaryFileResponse) {
292
            return;
293
        }
294
295 1
        $request = $event->getRequest();
296 1
        $cache_data = $response->getContent();
297
298
        // Generate E-Tag header.
299 1
        if (empty($cache_data)) {
300
            $etag = md5(serialize($this->_sent_headers));
301
        } else {
302 1
            $etag = md5($cache_data);
303
        }
304
305 1
        $response->setEtag($etag);
306
307
        // Register additional Headers around the current output request
308
        // It has been sent already during calls to content_type
309 1
        $this->register_sent_header('Content-Type', $this->_content_type);
310 1
        $this->complete_sent_headers($response);
311
312 1
        $response->prepare($request);
313
314
        /**
315
         * WARNING:
316
         *   Stuff below here is executed *after* we have flushed output,
317
         *   so here we should only write out our caches but do nothing else
318
         */
319 1
         if ($this->_uncached) {
320
             debug_add('Not writing cache file, we are in uncached operation mode.');
321
             return;
322
         }
323 1
         $content_id = 'C-' . $etag;
324 1
         $this->write_meta_cache($content_id, $etag, $request);
325 1
         $this->_data_cache->save($content_id, $cache_data);
326 1
    }
327
328
    /**
329
     * Generate a valid cache identifier for a context of the current request
330
     */
331 1
    private function generate_request_identifier(Request $request)
332
    {
333 1
        $context = $request->attributes->get('context')->id;
334
        // Cache the request identifier so that it doesn't change between start and end of request
335 1
        static $identifier_cache = [];
336 1
        if (isset($identifier_cache[$context])) {
337 1
            return $identifier_cache[$context];
338
        }
339
340 1
        $module_name = midcom::get()->config->get('cache_module_content_name');
341 1
        if ($module_name == 'auto') {
342 1
            $module_name = midcom_connection::get_unique_host_name();
343
        }
344 1
        $identifier_source = 'CACHE:' . $module_name;
345
346 1
        $cache_strategy = midcom::get()->config->get('cache_module_content_caching_strategy');
347
348
        switch ($cache_strategy) {
349 1
            case 'memberships':
350
                if (!midcom_connection::get_user()) {
351
                    $identifier_source .= ';USER=ANONYMOUS';
352
                    break;
353
                }
354
                $mc = new midgard_collector('midgard_member', 'uid', midcom_connection::get_user());
355
                $mc->set_key_property('gid');
356
                $mc->execute();
357
                $gids = $mc->list_keys();
358
                $identifier_source .= ';GROUPS=' . implode(',', array_keys($gids));
359
                break;
360 1
            case 'public':
361
                $identifier_source .= ';USER=EVERYONE';
362
                break;
363 1
            case 'user':
364
            default:
365 1
                $identifier_source .= ';USER=' . midcom_connection::get_user();
366 1
                break;
367
        }
368
369 1
        $identifier_source .= ';URL=' . $request->getRequestUri();
370 1
        debug_add("Generating context {$context} request-identifier from: {$identifier_source}");
371
372 1
        $identifier_cache[$context] = 'R-' . md5($identifier_source);
373 1
        return $identifier_cache[$context];
374
    }
375
376
    /**
377
     * Initialize the cache.
378
     *
379
     * The first step is to initialize the cache backends. The names of the
380
     * cache backends used for meta and data storage are derived from the name
381
     * defined for this module (see the 'name' configuration parameter above).
382
     * The name is used directly for the meta data cache, while the actual data
383
     * is stored in a backend postfixed with '_data'.
384
     *
385
     * After core initialization, the module checks for a cache hit (which might
386
     * trigger the delivery of the cached page and exit) and start the output buffer
387
     * afterwards.
388
     */
389 1
    public function _on_initialize()
390
    {
391 1
        $backend_config = midcom::get()->config->get('cache_module_content_backend');
392 1
        if (!isset($backend_config['directory'])) {
393 1
            $backend_config['directory'] = 'content/';
394
        }
395 1
        if (!isset($backend_config['driver'])) {
396
            $backend_config['driver'] = 'null';
397
        }
398
399 1
        $this->_meta_cache = $this->_create_backend('content_meta', $backend_config);
400 1
        $this->_data_cache = $this->_create_backend('content_data', $backend_config);
401
402 1
        $this->_uncached = midcom::get()->config->get('cache_module_content_uncached');
403 1
        $this->_headers_strategy = $this->get_strategy('cache_module_content_headers_strategy');
404 1
        $this->_headers_strategy_authenticated = $this->get_strategy('cache_module_content_headers_strategy_authenticated');
405 1
        $this->_default_lifetime = (int)midcom::get()->config->get('cache_module_content_default_lifetime');
406 1
        $this->_default_lifetime_authenticated = (int)midcom::get()->config->get('cache_module_content_default_lifetime_authenticated');
407 1
        $this->_force_headers = midcom::get()->config->get('cache_module_content_headers_force');
408
409 1
        if ($this->_headers_strategy == 'no-cache') {
410
            $this->no_cache();
411
        }
412 1
    }
413
414 1
    private function get_strategy($name)
415
    {
416 1
        $strategy = strtolower(midcom::get()->config->get($name));
417 1
        $allowed = ['no-cache', 'revalidate', 'public', 'private'];
418 1
        if (!in_array($strategy, $allowed)) {
419
            throw new midcom_error($name . ' is not valid, try ' . implode(', ', $allowed));
420
        }
421 1
        return $strategy;
422
    }
423
424
    /**
425
     * Call this, if the currently processed output must not be cached for any
426
     * reason. Dynamic pages with sensitive content are a candidate for this
427
     * function.
428
     *
429
     * Note, that this will prevent <i>any</i> content invalidation related headers
430
     * like E-Tag to be generated automatically, and that the appropriate
431
     * no-store/no-cache headers from HTTP 1.1 and HTTP 1.0 will be sent automatically.
432
     * This means that there will also be no 304 processing.
433
     *
434
     * You should use this only for sensitive content. For simple dynamic output,
435
     * you are strongly encouraged to use the less strict uncached() function.
436
     *
437
     * @see uncached()
438
     */
439 191
    public function no_cache(Response $response = null)
440
    {
441 191
        $settings = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0';
442
        // PONDER: Send expires header (set to long time in past) as well ??
443
444 191
        if ($response) {
445
            $response->headers->set('Cache-Control', $settings);
446 191
        } else if (!$this->_no_cache) {
447
            if (_midcom_headers_sent()) {
448
                // Whatever is wrong here, we return.
449
                debug_add('Warning, we should move to no_cache but headers have already been sent, skipping header transmission.', MIDCOM_LOG_ERROR);
450
                return;
451
            }
452
453
            _midcom_header('Cache-Control: ' . $settings);
454
        }
455 191
        $this->_no_cache = true;
456 191
    }
457
458
    /**
459
     * Call this, if the currently processed output must not be cached for any
460
     * reason. Dynamic pages or form processing results are the usual candidates
461
     * for this mode.
462
     *
463
     * Note, that this will still keep the caching engine active so that it can
464
     * add the usual headers (ETag, Expires ...) in respect to the no_cache flag.
465
     * As well, at the end of the processing, the usual 304 checks are done, so if
466
     * your page doesn't change in respect of E-Tag and Last-Modified, only a 304
467
     * Not Modified reaches the client.
468
     *
469
     * Essentially, no_cache behaves the same way as if the uncached configuration
470
     * directive is set to true, it is just limited to a single request.
471
     *
472
     * If you need a higher level of client side security, to avoid storage of sensitive
473
     * information on the client side, you should use no_cache instead.
474
     *
475
     * @see no_cache()
476
     */
477 3
    public function uncached($uncached = true)
478
    {
479 3
        $this->_uncached = $uncached;
480 3
    }
481
482
    /**
483
     * Sets the expiration time of the current page (Unix (GMT) Timestamp).
484
     *
485
     * <b>Note:</B> This generate error call will add browser-side cache control
486
     * headers as well to force a browser to revalidate a page after the set
487
     * expiry.
488
     *
489
     * You should call this at all places where you have timed content in your
490
     * output, so that the page will be regenerated once a certain article has
491
     * expired.
492
     *
493
     * Multiple calls to expires will only save the
494
     * "youngest" timestamp, so you can safely call expires where appropriate
495
     * without respect to other values.
496
     *
497
     * The cache's default (null) will disable the expires header. Note, that once
498
     * an expiry time on a page has been set, it is not possible, to reset it again,
499
     * this is for dynamic_load situation, where one component might depend on a
500
     * set expiry.
501
     *
502
     * @param int $timestamp The UNIX timestamp from which the cached page should be invalidated.
503
     */
504
    public function expires($timestamp)
505
    {
506
        if (   $this->_expires === null
507
            || $this->_expires > $timestamp) {
508
            $this->_expires = $timestamp;
509
        }
510
    }
511
512
    /**
513
     * Sets the content type for the current page. The required HTTP Headers for
514
     * are automatically generated, so, to the contrary of expires, you just have
515
     * to set this header accordingly.
516
     *
517
     * This is usually set automatically by MidCOM for all regular HTML output and
518
     * for all attachment deliveries. You have to adapt it only for things like RSS
519
     * output.
520
     *
521
     * @param string $type    The content type to use.
522
     */
523 10
    public function content_type($type)
524
    {
525 10
        $this->_content_type = $type;
526
527
        // Send header (don't register yet to avoid duplicates, this is done during finish
528
        // caching).
529 10
        $header = "Content-type: " . $this->_content_type;
530 10
        _midcom_header($header);
531 10
    }
532
533
    /**
534
     * Put the cache into a "live mode". This will disable the
535
     * cache during runtime, correctly flushing the output buffer (if it's not empty)
536
     * and sending cache control headers.
537
     *
538
     * The midcom-exec URL handler of the core will automatically enable live mode.
539
     *
540
     * @see midcom_application::_exec_file()
541
     */
542
    public function enable_live_mode()
543
    {
544
        $this->no_cache();
545
        Response::closeOutputBuffers(0, ob_get_length() > 0);
546
    }
547
548
    /**
549
     * Store a sent header into the cache database, so that it will
550
     * be resent when the cache page is delivered. midcom_application::header()
551
     * will automatically call this function, you need to do this only if you use
552
     * the PHP header function.
553
     *
554
     * @param string $header The header that was sent.
555
     * @param string $value
556
     */
557 16
    public function register_sent_header($header, $value = null)
558
    {
559 16
        if ($value === null && strpos($header, ': ') !== false) {
560 14
            $parts = explode(': ', $header, 2);
561 14
            $header = $parts[0];
562 14
            $value = $parts[1];
563
        }
564 16
        $this->_sent_headers[$header] = $value;
565 16
    }
566
567
    /**
568
     * Looks for list of content and request identifiers paired with the given guid
569
     * and removes all of those from the caches.
570
     *
571
     * {@inheritDoc}
572
     */
573 298
    public function invalidate($guid, $object = null)
574
    {
575 298
        $guidmap = $this->_meta_cache->fetch($guid);
576 298
        if ($guidmap === false) {
577 298
            debug_add("No entry for {$guid} in meta cache, ignoring invalidation request.");
578 298
            return;
579
        }
580
581
        foreach ($guidmap as $content_id) {
582
            if ($this->_meta_cache->contains($content_id)) {
583
                $this->_meta_cache->delete($content_id);
584
            }
585
586
            if ($this->_data_cache->contains($content_id)) {
587
                $this->_data_cache->delete($content_id);
588
            }
589
        }
590
    }
591
592
    /**
593
     * All objects loaded within a request are stored into a list for cache invalidation purposes
594
     */
595 424
    public function register($guid)
596
    {
597
        // Check for uncached operation
598 424
        if ($this->_uncached) {
599 424
            return;
600
        }
601
602
        $context = midcom_core_context::get()->id;
603
        if ($context != 0) {
604
            // We're in a dynamic_load, register it for that as well
605
            if (!isset($this->context_guids[$context])) {
606
                $this->context_guids[$context] = [];
607
            }
608
            $this->context_guids[$context][] = $guid;
609
        }
610
611
        // Register all GUIDs also to the root context
612
        if (!isset($this->context_guids[0])) {
613
            $this->context_guids[0] = [];
614
        }
615
        $this->context_guids[0][] = $guid;
616
    }
617
618
    /**
619
     * Writes meta-cache entry from context data using given content id
620
     * Used to be part of on_request, but needed by serve-attachment method in midcom_core_urlmethods as well
621
     */
622 1
    public function write_meta_cache($content_id, $etag, Request $request)
623
    {
624 1
        if (   $this->_uncached
625 1
            || $this->_no_cache) {
626
            return;
627
        }
628
629 1
        if ($this->_expires !== null) {
630
            $lifetime = $this->_expires - time();
631
        } else {
632
            // Use default expiry for cache entry, most components don't bother calling expires() properly
633 1
            $lifetime = $this->_default_lifetime;
634
        }
635
636
        // Construct cache identifier
637 1
        $request_id = $this->generate_request_identifier($request);
638
639
        $entries = [
640 1
            $request_id => $content_id,
641
            $content_id => [
642 1
                'etag' => $etag,
643 1
                'last_modified' => $this->_last_modified,
644 1
                'sent_headers' => $this->_sent_headers
645
            ]
646
        ];
647
648 1
        $this->_meta_cache->saveMultiple($entries, $lifetime);
649
650
        // Cache where the object have been
651 1
        $context = midcom_core_context::get()->id;
652 1
        $this->store_context_guid_map($context, $content_id, $request_id);
653 1
    }
654
655 1
    private function store_context_guid_map($context, $content_id, $request_id)
656
    {
657
        // non-existent context
658 1
        if (!array_key_exists($context, $this->context_guids)) {
659 1
            return;
660
        }
661
662
        $maps = $this->_meta_cache->fetchMultiple($this->context_guids[$context]);
663
        $to_save = [];
664
        foreach ($this->context_guids[$context] as $guid) {
665
            // Getting old map from cache or create new, empty one
666
            $guidmap = (empty($maps[$guid])) ? [] : $maps[$guid];
667
668
            if (!in_array($content_id, $guidmap)) {
669
                $guidmap[] = $content_id;
670
                $to_save[$guid] = $guidmap;
671
            }
672
673
            if (   $content_id !== $request_id
674
                && !in_array($request_id, $guidmap)) {
675
                $guidmap[] = $request_id;
676
                $to_save[$guid] = $guidmap;
677
            }
678
        }
679
680
        $this->_meta_cache->saveMultiple($to_save);
681
    }
682
683 16
    public function check_dl_hit(Request $request)
684
    {
685 16
        if ($this->_no_cache) {
686 16
            return false;
687
        }
688
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
689
        $dl_content_id = $this->_meta_cache->fetch($dl_request_id);
690
        if ($dl_content_id === false) {
691
            return false;
692
        }
693
694
        return $this->_data_cache->fetch($dl_content_id);
695
    }
696
697 4
    public function store_dl_content($context, $dl_cache_data, Request $request)
698
    {
699 4
        if (   $this->_no_cache
700 4
            || $this->_uncached) {
701 4
            return;
702
        }
703
        $dl_request_id = 'DL' . $this->generate_request_identifier($request);
704
        $dl_content_id = 'DLC-' . md5($dl_cache_data);
705
706
        if ($this->_expires !== null) {
707
            $lifetime = $this->_expires - time();
708
        } else {
709
            // Use default expiry for cache entry, most components don't bother calling expires() properly
710
            $lifetime = $this->_default_lifetime;
711
        }
712
        $this->_meta_cache->save($dl_request_id, $dl_content_id, $lifetime);
713
        $this->_data_cache->save($dl_content_id, $dl_cache_data, $lifetime);
714
        // Cache where the object have been
715
        $this->store_context_guid_map($context, $dl_content_id, $dl_request_id);
716
    }
717
718 1
    private function apply_headers(Response $response, array $headers)
719
    {
720 1
        foreach ($headers as $header => $value) {
721 1
            if ($value === null) {
722
                // compat for old-style midcom status setting
723
                _midcom_header($header);
724
            } else {
725 1
                $response->headers->set($header, $value);
726
            }
727
        }
728 1
    }
729
730
    /**
731
     * This little helper ensures that the headers Content-Length
732
     * and Last-Modified are present. The lastmod timestamp is taken out of the
733
     * component context information if it is populated correctly there; if not, the
734
     * system time is used instead.
735
     *
736
     * To force browsers to revalidate the page on every request (login changes would
737
     * go unnoticed otherwise), the Cache-Control header max-age=0 is added automatically.
738
     */
739 1
    private function complete_sent_headers(Response $response)
740
    {
741 1
        $this->apply_headers($response, $this->_sent_headers);
742
743
        // Detected headers flags
744 1
        $size = $response->headers->has('Content-Length');
745 1
        $lastmod = false;
746
747 1
        if ($date = $response->getLastModified()) {
748
            $lastmod = true;
749
            $this->_last_modified = (int) $date->format('U');
750
            if ($this->_last_modified == -1) {
751
                debug_add("Failed to extract the timecode from the last modified header, defaulting to the current time.", MIDCOM_LOG_WARN);
752
                $this->_last_modified = time();
753
            }
754
        }
755
756 1
        if (!$size) {
757
            /* TODO: Doublecheck the way this is handled, now we just don't send it
758
             * if headers_strategy implies caching */
759 1
            if (!in_array($this->_headers_strategy, ['public', 'private'])) {
760 1
                $response->headers->set("Content-Length", strlen($response->getContent()));
761 1
                $this->register_sent_header('Content-Length', $response->headers->get('Content-Length'));
762
            }
763
        }
764
765 1
        if (!$lastmod) {
766
            /* Determine Last-Modified using MidCOM's component context,
767
             * Fallback to time() if this fails.
768
             */
769 1
            $time = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_LASTMODIFIED);
770 1
            if ($time == 0 || !is_numeric($time)) {
771 1
                $time = time();
772
            }
773 1
            $response->setLastModified(DateTime::createFromFormat('U', $time));
0 ignored issues
show
Bug introduced by
It seems like DateTime::createFromFormat('U', $time) can also be of type false; however, parameter $date of Symfony\Component\HttpFo...onse::setLastModified() 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
            $response->setLastModified(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $time));
Loading history...
774 1
            $this->register_sent_header('Last-Modified', $response->headers->get('Last-Modified'));
775 1
            $this->_last_modified = $time;
776
        }
777
778 1
        $this->cache_control_headers($response);
779 1
        $this->register_sent_header('Cache-Control', $response->headers->get('Cache-Control'));
780 1
        if ($response->getExpires()) {
781 1
            $this->register_sent_header('Expires', $response->headers->get('Expires'));
782
        }
783 1
        if (is_array($this->_force_headers)) {
0 ignored issues
show
introduced by
The condition is_array($this->_force_headers) is always true.
Loading history...
784
            foreach ($this->_force_headers as $header => $value) {
785
                $response->headers->set($header, $value);
786
                $this->register_sent_header($header, $value);
787
            }
788
        }
789 1
    }
790
791
    /**
792
     * @param Response $response
793
     */
794 1
    public function cache_control_headers(Response $response)
795
    {
796
        // Just to be sure not to mess the headers sent by no_cache in case it was called
797 1
        if ($this->_no_cache) {
798
            $this->no_cache($response);
799
        } else {
800
            // Add Expiration and Cache Control headers
801 1
            $strategy = $this->_headers_strategy;
802 1
            $default_lifetime = $this->_default_lifetime;
803 1
            if (   midcom::get()->auth->is_valid_user()
804 1
                || midcom_connection::get_user()) {
805
                $strategy = $this->_headers_strategy_authenticated;
806
                $default_lifetime = $this->_default_lifetime_authenticated;
807
            }
808
809 1
            $now = time();
810 1
            if ($strategy == 'revalidate') {
811
                // If expires is not set, we force the client to revalidate every time.
812
                // The timeout of a content cache entry is not affected by this.
813 1
                $expires = $this->_expires ?? $now;
814
            } else {
815
                $expires = $this->_expires ?? $now + $default_lifetime;
816
                if ($strategy == 'private') {
817
                    $response->setPrivate();
818
                } else {
819
                    $response->setPublic();
820
                }
821
            }
822 1
            $max_age = $expires - $now;
823
824
            $response
825 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

825
                ->setExpires(/** @scrutinizer ignore-type */ DateTime::createFromFormat('U', $expires))
Loading history...
826 1
                ->setMaxAge($max_age);
827 1
            if ($max_age == 0) {
828 1
                $response->headers->addCacheControlDirective('must-revalidate');
829
            }
830
        }
831 1
    }
832
}
833