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