Merge pull request #12354 from totten/master-cache-b64
[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
e79e9c87
TO
43 /**
44 * @var array ($cacheKey => $cacheValue)
45 */
46 static $_cache = NULL;
47
6a488035 48 /**
fe482240 49 * Retrieve an item from the DB cache.
6a488035 50 *
6a0b768e
TO
51 * @param string $group
52 * (required) The group name of the item.
53 * @param string $path
54 * (required) The path under which this item is stored.
55 * @param int $componentID
56 * The optional component ID (so componenets can share the same name space).
6a488035 57 *
a6c01b45
CW
58 * @return object
59 * The data if present in cache, else null
6a488035 60 */
00be9182 61 public static function &getItem($group, $path, $componentID = NULL) {
e79e9c87
TO
62 if (self::$_cache === NULL) {
63 self::$_cache = array();
def0681b 64 }
6a488035 65
def0681b 66 $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
e79e9c87 67 if (!array_key_exists($argString, self::$_cache)) {
def0681b 68 $cache = CRM_Utils_Cache::singleton();
e79e9c87
TO
69 self::$_cache[$argString] = $cache->get($argString);
70 if (!self::$_cache[$argString]) {
61d29839
TO
71 $table = self::getTableName();
72 $where = self::whereCache($group, $path, $componentID);
5d1e8768 73 $rawData = CRM_Core_DAO::singleValueQuery("SELECT data FROM $table WHERE $where");
3c643fe7 74 $data = $rawData ? self::decode($rawData) : NULL;
61d29839 75
e79e9c87
TO
76 self::$_cache[$argString] = $data;
77 $cache->set($argString, self::$_cache[$argString]);
def0681b 78 }
6a488035 79 }
e79e9c87 80 return self::$_cache[$argString];
6a488035
TO
81 }
82
83 /**
fe482240 84 * Retrieve all items in a group.
6a488035 85 *
6a0b768e
TO
86 * @param string $group
87 * (required) The group name of the item.
88 * @param int $componentID
89 * The optional component ID (so componenets can share the same name space).
6a488035 90 *
a6c01b45
CW
91 * @return object
92 * The data if present in cache, else null
6a488035 93 */
00be9182 94 public static function &getItems($group, $componentID = NULL) {
e79e9c87
TO
95 if (self::$_cache === NULL) {
96 self::$_cache = array();
def0681b 97 }
6a488035 98
def0681b 99 $argString = "CRM_CT_CI_{$group}_{$componentID}";
e79e9c87 100 if (!array_key_exists($argString, self::$_cache)) {
def0681b 101 $cache = CRM_Utils_Cache::singleton();
e79e9c87
TO
102 self::$_cache[$argString] = $cache->get($argString);
103 if (!self::$_cache[$argString]) {
61d29839
TO
104 $table = self::getTableName();
105 $where = self::whereCache($group, NULL, $componentID);
106 $dao = CRM_Core_DAO::executeQuery("SELECT path, data FROM $table WHERE $where");
6a488035 107
1a4e6781 108 $result = array();
def0681b 109 while ($dao->fetch()) {
3c643fe7 110 $result[$dao->path] = self::decode($dao->data);
def0681b
KJ
111 }
112 $dao->free();
113
e79e9c87
TO
114 self::$_cache[$argString] = $result;
115 $cache->set($argString, self::$_cache[$argString]);
def0681b 116 }
6a488035 117 }
def0681b 118
e79e9c87 119 return self::$_cache[$argString];
6a488035
TO
120 }
121
122 /**
fe482240 123 * Store an item in the DB cache.
6a488035 124 *
6a0b768e
TO
125 * @param object $data
126 * (required) A reference to the data that will be serialized and stored.
127 * @param string $group
128 * (required) The group name of the item.
129 * @param string $path
130 * (required) The path under which this item is stored.
131 * @param int $componentID
132 * The optional component ID (so componenets can share the same name space).
6a488035 133 */
00be9182 134 public static function setItem(&$data, $group, $path, $componentID = NULL) {
e79e9c87
TO
135 if (self::$_cache === NULL) {
136 self::$_cache = array();
def0681b
KJ
137 }
138
6a488035
TO
139 // get a lock so that multiple ajax requests on the same page
140 // dont trample on each other
141 // CRM-11234
83617886 142 $lock = Civi::lockManager()->acquire("cache.{$group}_{$path}._{$componentID}");
6a488035
TO
143 if (!$lock->isAcquired()) {
144 CRM_Core_Error::fatal();
145 }
146
61d29839
TO
147 $table = self::getTableName();
148 $where = self::whereCache($group, $path, $componentID);
21ca2cb6 149 $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM $table WHERE {$where}");
61d29839 150 $now = date('Y-m-d H:i:s'); // FIXME - Use SQL NOW() or CRM_Utils_Time?
3c643fe7 151 $dataSerialized = self::encode($data);
61d29839
TO
152
153 // This table has a wonky index, so we cannot use REPLACE or
154 // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE).
21ca2cb6 155 if ($dataExists) {
156 $sql = "UPDATE $table SET data = %1, created_date = %2 WHERE {$where}";
3d6878c6 157 $args = array(
61d29839
TO
158 1 => array($dataSerialized, 'String'),
159 2 => array($now, 'String'),
3d6878c6 160 );
161 $dao = CRM_Core_DAO::executeQuery($sql, $args, TRUE, NULL, FALSE, FALSE);
61d29839
TO
162 }
163 else {
164 $insert = CRM_Utils_SQL_Insert::into($table)
165 ->row(array(
166 'group_name' => $group,
167 'path' => $path,
168 'component_id' => $componentID,
169 'data' => $dataSerialized,
170 'created_date' => $now,
171 ));
3d6878c6 172 $dao = CRM_Core_DAO::executeQuery($insert->toSQL(), array(), TRUE, NULL, FALSE, FALSE);
61d29839 173 }
6a488035
TO
174
175 $lock->release();
176
177 $dao->free();
def0681b 178
80259ba2
TO
179 // cache coherency - refresh or remove dependent caches
180
def0681b
KJ
181 $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
182 $cache = CRM_Utils_Cache::singleton();
3c643fe7 183 $data = self::decode($dataSerialized);
e79e9c87 184 self::$_cache[$argString] = $data;
def0681b 185 $cache->set($argString, $data);
80259ba2
TO
186
187 $argString = "CRM_CT_CI_{$group}_{$componentID}";
e79e9c87 188 unset(self::$_cache[$argString]);
80259ba2 189 $cache->delete($argString);
6a488035
TO
190 }
191
192 /**
1a4e6781 193 * Delete all the cache elements that belong to a group OR delete the entire cache if group is not specified.
6a488035 194 *
6a0b768e
TO
195 * @param string $group
196 * The group name of the entries to be deleted.
197 * @param string $path
198 * Path of the item that needs to be deleted.
1a4e6781 199 * @param bool $clearAll clear all caches
6a488035 200 */
00be9182 201 public static function deleteGroup($group = NULL, $path = NULL, $clearAll = TRUE) {
61d29839
TO
202 $table = self::getTableName();
203 $where = self::whereCache($group, $path, NULL);
204 CRM_Core_DAO::executeQuery("DELETE FROM $table WHERE $where");
6a488035
TO
205
206 if ($clearAll) {
207 // also reset ACL Cache
208 CRM_ACL_BAO_Cache::resetCache();
209
210 // also reset memory cache if any
211 CRM_Utils_System::flushCache();
212 }
213 }
214
215 /**
216 * The next two functions are internal functions used to store and retrieve session from
217 * the database cache. This keeps the session to a limited size and allows us to
218 * create separate session scopes for each form in a tab
6a488035
TO
219 */
220
221 /**
222 * This function takes entries from the session array and stores it in the cache.
1a4e6781 223 *
6a488035
TO
224 * It also deletes the entries from the $_SESSION object (for a smaller session size)
225 *
6a0b768e
TO
226 * @param array $names
227 * Array of session values that should be persisted.
6a488035
TO
228 * This is either a form name + qfKey or just a form name
229 * (in the case of profile)
6a0b768e
TO
230 * @param bool $resetSession
231 * Should session state be reset on completion of DB store?.
6a488035 232 */
00be9182 233 public static function storeSessionToCache($names, $resetSession = TRUE) {
6a488035
TO
234 foreach ($names as $key => $sessionName) {
235 if (is_array($sessionName)) {
2aa397bc 236 $value = NULL;
6a488035
TO
237 if (!empty($_SESSION[$sessionName[0]][$sessionName[1]])) {
238 $value = $_SESSION[$sessionName[0]][$sessionName[1]];
239 }
240 self::setItem($value, 'CiviCRM Session', "{$sessionName[0]}_{$sessionName[1]}");
2aa397bc
TO
241 if ($resetSession) {
242 $_SESSION[$sessionName[0]][$sessionName[1]] = NULL;
243 unset($_SESSION[$sessionName[0]][$sessionName[1]]);
6a488035 244 }
2aa397bc 245 }
6a488035 246 else {
2aa397bc 247 $value = NULL;
6a488035
TO
248 if (!empty($_SESSION[$sessionName])) {
249 $value = $_SESSION[$sessionName];
250 }
251 self::setItem($value, 'CiviCRM Session', $sessionName);
2aa397bc
TO
252 if ($resetSession) {
253 $_SESSION[$sessionName] = NULL;
254 unset($_SESSION[$sessionName]);
6a488035
TO
255 }
256 }
2aa397bc 257 }
6a488035
TO
258
259 self::cleanup();
260 }
261
262 /* Retrieve the session values from the cache and populate the $_SESSION array
006389de
TO
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)
006389de 268 */
6a488035 269
b5c2afd0 270 /**
1a4e6781
EM
271 * Restore session from cache.
272 *
100fef9d 273 * @param string $names
b5c2afd0 274 */
00be9182 275 public static function restoreSessionFromCache($names) {
6a488035
TO
276 foreach ($names as $key => $sessionName) {
277 if (is_array($sessionName)) {
278 $value = self::getItem('CiviCRM Session',
279 "{$sessionName[0]}_{$sessionName[1]}"
280 );
281 if ($value) {
282 $_SESSION[$sessionName[0]][$sessionName[1]] = $value;
283 }
284 }
285 else {
286 $value = self::getItem('CiviCRM Session',
287 $sessionName
288 );
289 if ($value) {
290 $_SESSION[$sessionName] = $value;
291 }
292 }
293 }
294 }
295
296 /**
1a4e6781
EM
297 * Do periodic cleanup of the CiviCRM session table.
298 *
299 * Also delete all session cache entries which are a couple of days old.
300 * This keeps the session cache to a manageable size
f26d31eb 301 * Delete Contribution page session caches more energetically.
6a488035 302 *
554259a7
EM
303 * @param bool $session
304 * @param bool $table
305 * @param bool $prevNext
6a488035 306 */
2aa397bc 307 public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FALSE) {
f26d31eb 308 // first delete all sessions more than 20 minutes old which are related to any potential transaction
8a8fb614 309 $timeIntervalMins = (int) Civi::settings()->get('secure_cache_timeout_minutes');
c98636df 310 if ($timeIntervalMins && $session) {
f26d31eb 311 $transactionPages = array(
312 'CRM_Contribute_Controller_Contribution',
313 'CRM_Event_Controller_Registration',
314 );
315
316 $params = array(
317 1 => array(
318 date('Y-m-d H:i:s', time() - $timeIntervalMins * 60),
319 'String',
320 ),
321 );
322 foreach ($transactionPages as $trPage) {
323 $params[] = array("%${trPage}%", 'String');
324 $where[] = 'path LIKE %' . count($params);
325 }
326
327 $sql = "
328DELETE FROM civicrm_cache
329WHERE group_name = 'CiviCRM Session'
330AND created_date <= %1
331AND (" . implode(' OR ', $where) . ")";
332 CRM_Core_DAO::executeQuery($sql, $params);
333 }
6a488035
TO
334 // clean up the session cache every $cacheCleanUpNumber probabilistically
335 $cleanUpNumber = 757;
336
337 // clean up all sessions older than $cacheTimeIntervalDays days
338 $timeIntervalDays = 2;
6a488035
TO
339
340 if (mt_rand(1, 100000) % $cleanUpNumber == 0) {
2aa397bc 341 $session = $table = $prevNext = TRUE;
6a488035
TO
342 }
343
f9f40af3 344 if (!$session && !$table && !$prevNext) {
6a488035
TO
345 return;
346 }
347
f9f40af3 348 if ($prevNext) {
6a488035
TO
349 // delete all PrevNext caches
350 CRM_Core_BAO_PrevNextCache::cleanupCache();
351 }
352
f9f40af3 353 if ($table) {
40c712ed 354 CRM_Core_Config::clearTempTables($timeIntervalDays . ' day');
6a488035
TO
355 }
356
f9f40af3 357 if ($session) {
6a488035
TO
358
359 $sql = "
360DELETE FROM civicrm_cache
361WHERE group_name = 'CiviCRM Session'
362AND created_date < date_sub( NOW( ), INTERVAL $timeIntervalDays DAY )
363";
364 CRM_Core_DAO::executeQuery($sql);
8c52547a 365 }
6a488035 366 }
96025800 367
3c643fe7
TO
368 /**
369 * (Quasi-private) Encode an object/array/string/int as a string.
370 *
371 * @param $mixed
372 * @return string
373 */
374 public static function encode($mixed) {
375 return base64_encode(serialize($mixed));
376 }
377
378 /**
379 * (Quasi-private) Decode an object/array/string/int from a string.
380 *
381 * @param $string
382 * @return mixed
383 */
384 public static function decode($string) {
385 // Upgrade support -- old records (serialize) always have this punctuation,
386 // and new records (base64) never do.
387 if (strpos($string, ':') !== FALSE || strpos($string, ';') !== FALSE) {
388 return unserialize($string);
389 }
390 else {
391 return unserialize(base64_decode($string));
392 }
393 }
394
61d29839
TO
395 /**
396 * Compose a SQL WHERE clause for the cache.
397 *
398 * Note: We need to use the cache during bootstrap, so we don't have
399 * full access to DAO services.
400 *
401 * @param string $group
402 * @param string|NULL $path
403 * Filter by path. If NULL, then return any paths.
404 * @param int|NULL $componentID
405 * Filter by component. If NULL, then look for explicitly NULL records.
406 * @return string
407 */
408 protected static function whereCache($group, $path, $componentID) {
409 $clauses = array();
410 $clauses[] = ('group_name = "' . CRM_Core_DAO::escapeString($group) . '"');
411 if ($path) {
412 $clauses[] = ('path = "' . CRM_Core_DAO::escapeString($path) . '"');
413 }
414 if ($componentID && is_numeric($componentID)) {
415 $clauses[] = ('component_id = ' . (int) $componentID);
416 }
417 return $clauses ? implode(' AND ', $clauses) : '(1)';
418 }
419
6a488035 420}