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