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