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