Merge pull request #16629 from WeMoveEU/core-1620
[civicrm-core.git] / CRM / Core / BAO / Cache.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035 11
6a488035 12/**
192d36c5 13 * BAO object for civicrm_cache table.
14 *
15 * This is a database cache and is persisted across sessions. Typically we use
6a488035
TO
16 * this to store meta data (like profile fields, custom fields etc).
17 *
18 * The group_name column is used for grouping together all cache elements that logically belong to the same set.
19 * Thus all session cache entries are grouped under 'CiviCRM Session'. This allows us to delete all entries of
20 * a specific group if needed.
21 *
22 * The path column allows us to differentiate between items in that group. Thus for the session cache, the path is
23 * the unique form name for each form (per user)
24 */
25class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache {
26
19707a63
TO
27 /**
28 * When store session/form state, how long should the data be retained?
29 *
041ecc95 30 * Default is Two days: 2*24*60*60
31 *
19707a63
TO
32 * @var int, number of second
33 */
cf9ccf98 34 const DEFAULT_SESSION_TTL = 172800;
19707a63 35
e79e9c87 36 /**
041ecc95 37 * Cache.
38 *
39 * Format is ($cacheKey => $cacheValue)
40 *
41 * @var array
e79e9c87 42 */
518fa0ee 43 public static $_cache = NULL;
e79e9c87 44
6a488035 45 /**
fe482240 46 * Retrieve an item from the DB cache.
6a488035 47 *
6a0b768e
TO
48 * @param string $group
49 * (required) The group name of the item.
50 * @param string $path
51 * (required) The path under which this item is stored.
52 * @param int $componentID
53 * The optional component ID (so componenets can share the same name space).
6a488035 54 *
a6c01b45
CW
55 * @return object
56 * The data if present in cache, else null
920fa38f 57 * @deprecated
6a488035 58 */
00be9182 59 public static function &getItem($group, $path, $componentID = NULL) {
cf4c32dd
SL
60 CRM_Core_Error::deprecatedFunctionWarning(
61 'CRM_Core_BAO_Cache::getItem is deprecated and will be removed from core soon, use Civi::cache() facade or define cache group using hook_civicrm_container'
62 );
88dfeb5c 63 if (($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) !== NULL) {
b5e7d576
TO
64 $value = $adapter::getItem($group, $path, $componentID);
65 return $value;
5a302bbc
TO
66 }
67
e79e9c87 68 if (self::$_cache === NULL) {
be2fb01f 69 self::$_cache = [];
def0681b 70 }
6a488035 71
def0681b 72 $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
e79e9c87 73 if (!array_key_exists($argString, self::$_cache)) {
def0681b 74 $cache = CRM_Utils_Cache::singleton();
972b0e25
CB
75 $cleanKey = self::cleanKey($argString);
76 self::$_cache[$argString] = $cache->get($cleanKey);
22263d41 77 if (self::$_cache[$argString] === NULL) {
61d29839
TO
78 $table = self::getTableName();
79 $where = self::whereCache($group, $path, $componentID);
5d1e8768 80 $rawData = CRM_Core_DAO::singleValueQuery("SELECT data FROM $table WHERE $where");
3c643fe7 81 $data = $rawData ? self::decode($rawData) : NULL;
61d29839 82
e79e9c87 83 self::$_cache[$argString] = $data;
ce18ad0f 84 if ($data !== NULL) {
85 // Do not cache 'null' as that is most likely a cache miss & we shouldn't then cache it.
86 $cache->set($cleanKey, self::$_cache[$argString]);
87 }
def0681b 88 }
6a488035 89 }
e79e9c87 90 return self::$_cache[$argString];
6a488035
TO
91 }
92
93 /**
fe482240 94 * Retrieve all items in a group.
6a488035 95 *
6a0b768e
TO
96 * @param string $group
97 * (required) The group name of the item.
98 * @param int $componentID
99 * The optional component ID (so componenets can share the same name space).
6a488035 100 *
a6c01b45
CW
101 * @return object
102 * The data if present in cache, else null
920fa38f 103 * @deprecated
6a488035 104 */
00be9182 105 public static function &getItems($group, $componentID = NULL) {
cf4c32dd
SL
106 CRM_Core_Error::deprecatedFunctionWarning(
107 'CRM_Core_BAO_Cache::getItems is deprecated and will be removed from core soon, use Civi::cache() facade or define cache group using hook_civicrm_container'
108 );
88dfeb5c 109 if (($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) !== NULL) {
5a302bbc
TO
110 return $adapter::getItems($group, $componentID);
111 }
112
e79e9c87 113 if (self::$_cache === NULL) {
be2fb01f 114 self::$_cache = [];
def0681b 115 }
6a488035 116
def0681b 117 $argString = "CRM_CT_CI_{$group}_{$componentID}";
e79e9c87 118 if (!array_key_exists($argString, self::$_cache)) {
def0681b 119 $cache = CRM_Utils_Cache::singleton();
972b0e25
CB
120 $cleanKey = self::cleanKey($argString);
121 self::$_cache[$argString] = $cache->get($cleanKey);
e79e9c87 122 if (!self::$_cache[$argString]) {
61d29839
TO
123 $table = self::getTableName();
124 $where = self::whereCache($group, NULL, $componentID);
125 $dao = CRM_Core_DAO::executeQuery("SELECT path, data FROM $table WHERE $where");
6a488035 126
be2fb01f 127 $result = [];
def0681b 128 while ($dao->fetch()) {
3c643fe7 129 $result[$dao->path] = self::decode($dao->data);
def0681b 130 }
def0681b 131
e79e9c87 132 self::$_cache[$argString] = $result;
972b0e25 133 $cache->set($cleanKey, self::$_cache[$argString]);
def0681b 134 }
6a488035 135 }
def0681b 136
e79e9c87 137 return self::$_cache[$argString];
6a488035
TO
138 }
139
140 /**
fe482240 141 * Store an item in the DB cache.
6a488035 142 *
6a0b768e
TO
143 * @param object $data
144 * (required) A reference to the data that will be serialized and stored.
145 * @param string $group
146 * (required) The group name of the item.
147 * @param string $path
148 * (required) The path under which this item is stored.
149 * @param int $componentID
150 * The optional component ID (so componenets can share the same name space).
920fa38f 151 * @deprecated
ac15829d 152 * @throws CRM_Core_Exception
6a488035 153 */
00be9182 154 public static function setItem(&$data, $group, $path, $componentID = NULL) {
cf4c32dd
SL
155 CRM_Core_Error::deprecatedFunctionWarning(
156 'CRM_Core_BAO_Cache::setItem is deprecated and will be removed from core soon, use Civi::cache() facade or define cache group using hook_civicrm_container'
157 );
88dfeb5c 158 if (($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) !== NULL) {
5a302bbc
TO
159 return $adapter::setItem($data, $group, $path, $componentID);
160 }
161
e79e9c87 162 if (self::$_cache === NULL) {
be2fb01f 163 self::$_cache = [];
def0681b
KJ
164 }
165
6a488035
TO
166 // get a lock so that multiple ajax requests on the same page
167 // dont trample on each other
168 // CRM-11234
83617886 169 $lock = Civi::lockManager()->acquire("cache.{$group}_{$path}._{$componentID}");
6a488035 170 if (!$lock->isAcquired()) {
ac15829d 171 throw new CRM_Core_Exception('Cannot acquire database lock');
6a488035
TO
172 }
173
61d29839
TO
174 $table = self::getTableName();
175 $where = self::whereCache($group, $path, $componentID);
21ca2cb6 176 $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM $table WHERE {$where}");
cf9ccf98
TO
177 // FIXME - Use SQL NOW() or CRM_Utils_Time?
178 $now = date('Y-m-d H:i:s');
3c643fe7 179 $dataSerialized = self::encode($data);
61d29839
TO
180
181 // This table has a wonky index, so we cannot use REPLACE or
182 // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE).
21ca2cb6 183 if ($dataExists) {
184 $sql = "UPDATE $table SET data = %1, created_date = %2 WHERE {$where}";
be2fb01f
CW
185 $args = [
186 1 => [$dataSerialized, 'String'],
187 2 => [$now, 'String'],
188 ];
3d6878c6 189 $dao = CRM_Core_DAO::executeQuery($sql, $args, TRUE, NULL, FALSE, FALSE);
61d29839
TO
190 }
191 else {
192 $insert = CRM_Utils_SQL_Insert::into($table)
be2fb01f 193 ->row([
61d29839
TO
194 'group_name' => $group,
195 'path' => $path,
196 'component_id' => $componentID,
197 'data' => $dataSerialized,
198 'created_date' => $now,
be2fb01f
CW
199 ]);
200 $dao = CRM_Core_DAO::executeQuery($insert->toSQL(), [], TRUE, NULL, FALSE, FALSE);
61d29839 201 }
6a488035
TO
202
203 $lock->release();
204
80259ba2
TO
205 // cache coherency - refresh or remove dependent caches
206
def0681b
KJ
207 $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
208 $cache = CRM_Utils_Cache::singleton();
3c643fe7 209 $data = self::decode($dataSerialized);
e79e9c87 210 self::$_cache[$argString] = $data;
bd76ee83 211 $cache->set(self::cleanKey($argString), $data);
80259ba2
TO
212
213 $argString = "CRM_CT_CI_{$group}_{$componentID}";
e79e9c87 214 unset(self::$_cache[$argString]);
bd76ee83 215 $cache->delete(self::cleanKey($argString));
6a488035
TO
216 }
217
218 /**
1a4e6781 219 * Delete all the cache elements that belong to a group OR delete the entire cache if group is not specified.
6a488035 220 *
6a0b768e
TO
221 * @param string $group
222 * The group name of the entries to be deleted.
223 * @param string $path
224 * Path of the item that needs to be deleted.
1a4e6781 225 * @param bool $clearAll clear all caches
920fa38f 226 * @deprecated
6a488035 227 */
00be9182 228 public static function deleteGroup($group = NULL, $path = NULL, $clearAll = TRUE) {
cf4c32dd
SL
229 CRM_Core_Error::deprecatedFunctionWarning(
230 'CRM_Core_BAO_Cache::deleteGroup is deprecated and will be removed from core soon, use Civi::cache() facade or define cache group using hook_civicrm_container'
231 );
88dfeb5c 232 if (($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) !== NULL) {
5a302bbc
TO
233 return $adapter::deleteGroup($group, $path);
234 }
235 else {
236 $table = self::getTableName();
237 $where = self::whereCache($group, $path, NULL);
238 CRM_Core_DAO::executeQuery("DELETE FROM $table WHERE $where");
239 }
6a488035
TO
240
241 if ($clearAll) {
33660480 242 self::resetCaches();
6a488035
TO
243 }
244 }
245
33660480
SL
246 /**
247 * Cleanup ACL and System Level caches
248 */
249 public static function resetCaches() {
250 // also reset ACL Cache
251 // @todo why is this called when CRM_Utils_System::flushCache() does it as well.
252 CRM_ACL_BAO_Cache::resetCache();
253
254 // also reset memory cache if any
255 CRM_Utils_System::flushCache();
256 }
257
6a488035
TO
258 /**
259 * The next two functions are internal functions used to store and retrieve session from
260 * the database cache. This keeps the session to a limited size and allows us to
261 * create separate session scopes for each form in a tab
6a488035
TO
262 */
263
264 /**
265 * This function takes entries from the session array and stores it in the cache.
1a4e6781 266 *
6a488035
TO
267 * It also deletes the entries from the $_SESSION object (for a smaller session size)
268 *
6a0b768e
TO
269 * @param array $names
270 * Array of session values that should be persisted.
6a488035
TO
271 * This is either a form name + qfKey or just a form name
272 * (in the case of profile)
6a0b768e
TO
273 * @param bool $resetSession
274 * Should session state be reset on completion of DB store?.
6a488035 275 */
00be9182 276 public static function storeSessionToCache($names, $resetSession = TRUE) {
6a488035
TO
277 foreach ($names as $key => $sessionName) {
278 if (is_array($sessionName)) {
2aa397bc 279 $value = NULL;
6a488035
TO
280 if (!empty($_SESSION[$sessionName[0]][$sessionName[1]])) {
281 $value = $_SESSION[$sessionName[0]][$sessionName[1]];
282 }
19707a63
TO
283 $key = "{$sessionName[0]}_{$sessionName[1]}";
284 Civi::cache('session')->set($key, $value, self::pickSessionTtl($key));
2aa397bc
TO
285 if ($resetSession) {
286 $_SESSION[$sessionName[0]][$sessionName[1]] = NULL;
287 unset($_SESSION[$sessionName[0]][$sessionName[1]]);
6a488035 288 }
2aa397bc 289 }
6a488035 290 else {
2aa397bc 291 $value = NULL;
6a488035
TO
292 if (!empty($_SESSION[$sessionName])) {
293 $value = $_SESSION[$sessionName];
294 }
19707a63 295 Civi::cache('session')->set($sessionName, $value, self::pickSessionTtl($sessionName));
2aa397bc
TO
296 if ($resetSession) {
297 $_SESSION[$sessionName] = NULL;
298 unset($_SESSION[$sessionName]);
6a488035
TO
299 }
300 }
2aa397bc 301 }
6a488035
TO
302
303 self::cleanup();
304 }
305
306 /* Retrieve the session values from the cache and populate the $_SESSION array
006389de
TO
307 *
308 * @param array $names
309 * Array of session values that should be persisted.
310 * This is either a form name + qfKey or just a form name
311 * (in the case of profile)
006389de 312 */
6a488035 313
b5c2afd0 314 /**
1a4e6781
EM
315 * Restore session from cache.
316 *
100fef9d 317 * @param string $names
b5c2afd0 318 */
00be9182 319 public static function restoreSessionFromCache($names) {
6a488035
TO
320 foreach ($names as $key => $sessionName) {
321 if (is_array($sessionName)) {
19707a63 322 $value = Civi::cache('session')->get("{$sessionName[0]}_{$sessionName[1]}");
6a488035
TO
323 if ($value) {
324 $_SESSION[$sessionName[0]][$sessionName[1]] = $value;
325 }
326 }
327 else {
19707a63 328 $value = Civi::cache('session')->get($sessionName);
6a488035
TO
329 if ($value) {
330 $_SESSION[$sessionName] = $value;
331 }
332 }
333 }
334 }
335
19707a63
TO
336 /**
337 * Determine how long session-state should be retained.
338 *
339 * @param string $sessionKey
340 * Ex: '_CRM_Admin_Form_Preferences_Display_f1a5f232e3d850a29a7a4d4079d7c37b_4654_container'
341 * Ex: 'CiviCRM_CRM_Admin_Form_Preferences_Display_f1a5f232e3d850a29a7a4d4079d7c37b_4654'
342 * @return int
343 * Number of seconds.
344 */
345 protected static function pickSessionTtl($sessionKey) {
346 $secureSessionTimeoutMinutes = (int) Civi::settings()->get('secure_cache_timeout_minutes');
347 if ($secureSessionTimeoutMinutes) {
be2fb01f 348 $transactionPages = [
19707a63
TO
349 'CRM_Contribute_Controller_Contribution',
350 'CRM_Event_Controller_Registration',
be2fb01f 351 ];
19707a63
TO
352 foreach ($transactionPages as $transactionPage) {
353 if (strpos($sessionKey, $transactionPage) !== FALSE) {
354 return $secureSessionTimeoutMinutes * 60;
355 }
356 }
357 }
358
359 return self::DEFAULT_SESSION_TTL;
360 }
361
6a488035 362 /**
1a4e6781
EM
363 * Do periodic cleanup of the CiviCRM session table.
364 *
365 * Also delete all session cache entries which are a couple of days old.
366 * This keeps the session cache to a manageable size
f26d31eb 367 * Delete Contribution page session caches more energetically.
6a488035 368 *
554259a7
EM
369 * @param bool $session
370 * @param bool $table
371 * @param bool $prevNext
518fa0ee 372 * @param bool $expired
6a488035 373 */
fd33fead 374 public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FALSE, $expired = FALSE) {
6a488035
TO
375 // clean up the session cache every $cacheCleanUpNumber probabilistically
376 $cleanUpNumber = 757;
377
378 // clean up all sessions older than $cacheTimeIntervalDays days
379 $timeIntervalDays = 2;
6a488035
TO
380
381 if (mt_rand(1, 100000) % $cleanUpNumber == 0) {
fd33fead 382 $expired = $session = $table = $prevNext = TRUE;
6a488035
TO
383 }
384
fd33fead 385 if (!$session && !$table && !$prevNext && !$expired) {
6a488035
TO
386 return;
387 }
388
f9f40af3 389 if ($prevNext) {
6a488035 390 // delete all PrevNext caches
b3e4463a 391 Civi::service('prevnext')->cleanup();
6a488035
TO
392 }
393
f9f40af3 394 if ($table) {
40c712ed 395 CRM_Core_Config::clearTempTables($timeIntervalDays . ' day');
6a488035
TO
396 }
397
f9f40af3 398 if ($session) {
19707a63
TO
399 // Session caches are just regular caches, so they expire naturally per TTL.
400 $expired = TRUE;
8c52547a 401 }
fd33fead
TO
402
403 if ($expired) {
404 $sql = "DELETE FROM civicrm_cache WHERE expired_date < %1";
405 $params = [
406 1 => [date(CRM_Utils_Cache_SqlGroup::TS_FMT, CRM_Utils_Time::getTimeRaw()), 'String'],
407 ];
408 CRM_Core_DAO::executeQuery($sql, $params);
409 }
6a488035 410 }
96025800 411
3c643fe7
TO
412 /**
413 * (Quasi-private) Encode an object/array/string/int as a string.
414 *
415 * @param $mixed
416 * @return string
417 */
418 public static function encode($mixed) {
419 return base64_encode(serialize($mixed));
420 }
421
422 /**
423 * (Quasi-private) Decode an object/array/string/int from a string.
424 *
425 * @param $string
426 * @return mixed
427 */
428 public static function decode($string) {
429 // Upgrade support -- old records (serialize) always have this punctuation,
430 // and new records (base64) never do.
431 if (strpos($string, ':') !== FALSE || strpos($string, ';') !== FALSE) {
432 return unserialize($string);
433 }
434 else {
435 return unserialize(base64_decode($string));
436 }
437 }
438
61d29839
TO
439 /**
440 * Compose a SQL WHERE clause for the cache.
441 *
442 * Note: We need to use the cache during bootstrap, so we don't have
443 * full access to DAO services.
444 *
445 * @param string $group
e97c66ff 446 * @param string|null $path
61d29839 447 * Filter by path. If NULL, then return any paths.
e97c66ff 448 * @param int|null $componentID
61d29839
TO
449 * Filter by component. If NULL, then look for explicitly NULL records.
450 * @return string
451 */
452 protected static function whereCache($group, $path, $componentID) {
be2fb01f 453 $clauses = [];
61d29839
TO
454 $clauses[] = ('group_name = "' . CRM_Core_DAO::escapeString($group) . '"');
455 if ($path) {
456 $clauses[] = ('path = "' . CRM_Core_DAO::escapeString($path) . '"');
457 }
458 if ($componentID && is_numeric($componentID)) {
459 $clauses[] = ('component_id = ' . (int) $componentID);
460 }
461 return $clauses ? implode(' AND ', $clauses) : '(1)';
462 }
463
bd76ee83
TO
464 /**
465 * Normalize a cache key.
466 *
467 * This bridges an impedance mismatch between our traditional caching
468 * and PSR-16 -- PSR-16 accepts a narrower range of cache keys.
469 *
470 * @param string $key
471 * Ex: 'ab/cd:ef'
472 * @return string
473 * Ex: '_abcd1234abcd1234' or 'ab_xx/cd_xxef'.
474 * A similar key, but suitable for use with PSR-16-compliant cache providers.
77f080cb
TO
475 * @deprecated
476 * @see CRM_Utils_Cache::cleanKey()
bd76ee83
TO
477 */
478 public static function cleanKey($key) {
77f080cb 479 return CRM_Utils_Cache::cleanKey($key);
bd76ee83
TO
480 }
481
6a488035 482}