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