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