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