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