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