Merge pull request #9594 from alifrumin/crm19807
[civicrm-core.git] / CRM / Core / BAO / Cache.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
0f03f337 6 | Copyright CiviCRM LLC (c) 2004-2017 |
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");
74 $data = $rawData ? unserialize($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
KJ
109 while ($dao->fetch()) {
110 $result[$dao->path] = unserialize($dao->data);
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);
149 $id = CRM_Core_DAO::singleValueQuery("SELECT id FROM $table WHERE $where");
150 $now = date('Y-m-d H:i:s'); // FIXME - Use SQL NOW() or CRM_Utils_Time?
151 $dataSerialized = serialize($data);
152
153 // This table has a wonky index, so we cannot use REPLACE or
154 // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE).
155 if ($id) {
156 $sql = "UPDATE $table SET data = %1, created_date = %2 WHERE id = %3";
3d6878c6 157 $args = array(
61d29839
TO
158 1 => array($dataSerialized, 'String'),
159 2 => array($now, 'String'),
160 3 => array($id, 'Int'),
3d6878c6 161 );
162 $dao = CRM_Core_DAO::executeQuery($sql, $args, TRUE, NULL, FALSE, FALSE);
61d29839
TO
163 }
164 else {
165 $insert = CRM_Utils_SQL_Insert::into($table)
166 ->row(array(
167 'group_name' => $group,
168 'path' => $path,
169 'component_id' => $componentID,
170 'data' => $dataSerialized,
171 'created_date' => $now,
172 ));
3d6878c6 173 $dao = CRM_Core_DAO::executeQuery($insert->toSQL(), array(), TRUE, NULL, FALSE, FALSE);
61d29839 174 }
6a488035
TO
175
176 $lock->release();
177
178 $dao->free();
def0681b 179
80259ba2
TO
180 // cache coherency - refresh or remove dependent caches
181
def0681b
KJ
182 $argString = "CRM_CT_{$group}_{$path}_{$componentID}";
183 $cache = CRM_Utils_Cache::singleton();
61d29839 184 $data = unserialize($dataSerialized);
e79e9c87 185 self::$_cache[$argString] = $data;
def0681b 186 $cache->set($argString, $data);
80259ba2
TO
187
188 $argString = "CRM_CT_CI_{$group}_{$componentID}";
e79e9c87 189 unset(self::$_cache[$argString]);
80259ba2 190 $cache->delete($argString);
6a488035
TO
191 }
192
193 /**
1a4e6781 194 * Delete all the cache elements that belong to a group OR delete the entire cache if group is not specified.
6a488035 195 *
6a0b768e
TO
196 * @param string $group
197 * The group name of the entries to be deleted.
198 * @param string $path
199 * Path of the item that needs to be deleted.
1a4e6781 200 * @param bool $clearAll clear all caches
6a488035 201 */
00be9182 202 public static function deleteGroup($group = NULL, $path = NULL, $clearAll = TRUE) {
61d29839
TO
203 $table = self::getTableName();
204 $where = self::whereCache($group, $path, NULL);
205 CRM_Core_DAO::executeQuery("DELETE FROM $table WHERE $where");
6a488035
TO
206
207 if ($clearAll) {
208 // also reset ACL Cache
209 CRM_ACL_BAO_Cache::resetCache();
210
211 // also reset memory cache if any
212 CRM_Utils_System::flushCache();
213 }
214 }
215
216 /**
217 * The next two functions are internal functions used to store and retrieve session from
218 * the database cache. This keeps the session to a limited size and allows us to
219 * create separate session scopes for each form in a tab
6a488035
TO
220 */
221
222 /**
223 * This function takes entries from the session array and stores it in the cache.
1a4e6781 224 *
6a488035
TO
225 * It also deletes the entries from the $_SESSION object (for a smaller session size)
226 *
6a0b768e
TO
227 * @param array $names
228 * Array of session values that should be persisted.
6a488035
TO
229 * This is either a form name + qfKey or just a form name
230 * (in the case of profile)
6a0b768e
TO
231 * @param bool $resetSession
232 * Should session state be reset on completion of DB store?.
6a488035 233 */
00be9182 234 public static function storeSessionToCache($names, $resetSession = TRUE) {
6a488035
TO
235 foreach ($names as $key => $sessionName) {
236 if (is_array($sessionName)) {
2aa397bc 237 $value = NULL;
6a488035
TO
238 if (!empty($_SESSION[$sessionName[0]][$sessionName[1]])) {
239 $value = $_SESSION[$sessionName[0]][$sessionName[1]];
240 }
241 self::setItem($value, 'CiviCRM Session', "{$sessionName[0]}_{$sessionName[1]}");
2aa397bc
TO
242 if ($resetSession) {
243 $_SESSION[$sessionName[0]][$sessionName[1]] = NULL;
244 unset($_SESSION[$sessionName[0]][$sessionName[1]]);
6a488035 245 }
2aa397bc 246 }
6a488035 247 else {
2aa397bc 248 $value = NULL;
6a488035
TO
249 if (!empty($_SESSION[$sessionName])) {
250 $value = $_SESSION[$sessionName];
251 }
252 self::setItem($value, 'CiviCRM Session', $sessionName);
2aa397bc
TO
253 if ($resetSession) {
254 $_SESSION[$sessionName] = NULL;
255 unset($_SESSION[$sessionName]);
6a488035
TO
256 }
257 }
2aa397bc 258 }
6a488035
TO
259
260 self::cleanup();
261 }
262
263 /* Retrieve the session values from the cache and populate the $_SESSION array
006389de
TO
264 *
265 * @param array $names
266 * Array of session values that should be persisted.
267 * This is either a form name + qfKey or just a form name
268 * (in the case of profile)
006389de 269 */
6a488035 270
b5c2afd0 271 /**
1a4e6781
EM
272 * Restore session from cache.
273 *
100fef9d 274 * @param string $names
b5c2afd0 275 */
00be9182 276 public static function restoreSessionFromCache($names) {
6a488035
TO
277 foreach ($names as $key => $sessionName) {
278 if (is_array($sessionName)) {
279 $value = self::getItem('CiviCRM Session',
280 "{$sessionName[0]}_{$sessionName[1]}"
281 );
282 if ($value) {
283 $_SESSION[$sessionName[0]][$sessionName[1]] = $value;
284 }
285 }
286 else {
287 $value = self::getItem('CiviCRM Session',
288 $sessionName
289 );
290 if ($value) {
291 $_SESSION[$sessionName] = $value;
292 }
293 }
294 }
295 }
296
297 /**
1a4e6781
EM
298 * Do periodic cleanup of the CiviCRM session table.
299 *
300 * Also delete all session cache entries which are a couple of days old.
301 * This keeps the session cache to a manageable size
f26d31eb 302 * Delete Contribution page session caches more energetically.
6a488035 303 *
554259a7
EM
304 * @param bool $session
305 * @param bool $table
306 * @param bool $prevNext
6a488035 307 */
2aa397bc 308 public static function cleanup($session = FALSE, $table = FALSE, $prevNext = FALSE) {
f26d31eb 309 // first delete all sessions more than 20 minutes old which are related to any potential transaction
8a8fb614 310 $timeIntervalMins = (int) Civi::settings()->get('secure_cache_timeout_minutes');
c98636df 311 if ($timeIntervalMins && $session) {
f26d31eb 312 $transactionPages = array(
313 'CRM_Contribute_Controller_Contribution',
314 'CRM_Event_Controller_Registration',
315 );
316
317 $params = array(
318 1 => array(
319 date('Y-m-d H:i:s', time() - $timeIntervalMins * 60),
320 'String',
321 ),
322 );
323 foreach ($transactionPages as $trPage) {
324 $params[] = array("%${trPage}%", 'String');
325 $where[] = 'path LIKE %' . count($params);
326 }
327
328 $sql = "
329DELETE FROM civicrm_cache
330WHERE group_name = 'CiviCRM Session'
331AND created_date <= %1
332AND (" . implode(' OR ', $where) . ")";
333 CRM_Core_DAO::executeQuery($sql, $params);
334 }
6a488035
TO
335 // clean up the session cache every $cacheCleanUpNumber probabilistically
336 $cleanUpNumber = 757;
337
338 // clean up all sessions older than $cacheTimeIntervalDays days
339 $timeIntervalDays = 2;
6a488035
TO
340
341 if (mt_rand(1, 100000) % $cleanUpNumber == 0) {
2aa397bc 342 $session = $table = $prevNext = TRUE;
6a488035
TO
343 }
344
f9f40af3 345 if (!$session && !$table && !$prevNext) {
6a488035
TO
346 return;
347 }
348
f9f40af3 349 if ($prevNext) {
6a488035
TO
350 // delete all PrevNext caches
351 CRM_Core_BAO_PrevNextCache::cleanupCache();
352 }
353
f9f40af3 354 if ($table) {
40c712ed 355 CRM_Core_Config::clearTempTables($timeIntervalDays . ' day');
6a488035
TO
356 }
357
f9f40af3 358 if ($session) {
6a488035
TO
359
360 $sql = "
361DELETE FROM civicrm_cache
362WHERE group_name = 'CiviCRM Session'
363AND created_date < date_sub( NOW( ), INTERVAL $timeIntervalDays DAY )
364";
365 CRM_Core_DAO::executeQuery($sql);
8c52547a 366 }
6a488035 367 }
96025800 368
61d29839
TO
369 /**
370 * Compose a SQL WHERE clause for the cache.
371 *
372 * Note: We need to use the cache during bootstrap, so we don't have
373 * full access to DAO services.
374 *
375 * @param string $group
376 * @param string|NULL $path
377 * Filter by path. If NULL, then return any paths.
378 * @param int|NULL $componentID
379 * Filter by component. If NULL, then look for explicitly NULL records.
380 * @return string
381 */
382 protected static function whereCache($group, $path, $componentID) {
383 $clauses = array();
384 $clauses[] = ('group_name = "' . CRM_Core_DAO::escapeString($group) . '"');
385 if ($path) {
386 $clauses[] = ('path = "' . CRM_Core_DAO::escapeString($path) . '"');
387 }
388 if ($componentID && is_numeric($componentID)) {
389 $clauses[] = ('component_id = ' . (int) $componentID);
390 }
391 return $clauses ? implode(' AND ', $clauses) : '(1)';
392 }
393
6a488035 394}