Merge pull request #48 from dpradeep/merge-forward
[civicrm-core.git] / CRM / Contact / BAO / GroupContactCache.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
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 +--------------------------------------------------------------------+
26*/
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35class CRM_Contact_BAO_GroupContactCache extends CRM_Contact_DAO_GroupContactCache {
36
37 static $_alreadyLoaded = array();
38
39 /**
40 * Check to see if we have cache entries for this group
41 * if not, regenerate, else return
42 *
77b97be7
EM
43 * @param $groupIDs
44 *
45 * @internal param int $groupID groupID of group that we are checking against
6a488035
TO
46 *
47 * @return boolean true if we did not regenerate, false if we did
48 */
49 static function check($groupIDs) {
50 if (empty($groupIDs)) {
51 return TRUE;
52 }
53
54 return self::loadAll($groupIDs);
55 }
56
abdb2607
DL
57 /**
58 * Common function that formulates the query to see which groups needs to be refreshed
59 * based on their cache date and the smartGroupCacheTimeOut
60 *
61 * @param string $groupIDClause the clause which limits which groups we need to evaluate
62 * @param boolean $includeHiddenGroups hidden groups are excluded by default
63 *
64 * @return string the sql query which lists the groups that need to be refreshed
65 * @static
66 * @public
67 */
68 static function groupRefreshedClause($groupIDClause = null, $includeHiddenGroups = FALSE) {
69 $smartGroupCacheTimeout = self::smartGroupCacheTimeout();
70 $now = CRM_Utils_Date::getUTCTime();
71
72 $query = "
73SELECT g.id
74FROM civicrm_group g
75WHERE ( g.saved_search_id IS NOT NULL OR g.children IS NOT NULL )
76AND g.is_active = 1
77AND ( g.cache_date IS NULL OR
78 ( TIMESTAMPDIFF(MINUTE, g.cache_date, $now) >= $smartGroupCacheTimeout ) OR
79 ( $now >= g.refresh_date )
80 )
81";
82
83 if (!$includeHiddenGroups) {
84 $query .= "AND (g.is_hidden = 0 OR g.is_hidden IS NULL)";
85 }
86
87 if (!empty($groupIDClause)) {
88 $query .= " AND ( $groupIDClause ) ";
89 }
90
91 return $query;
92 }
93
94 /**
95 * Checks to see if a group has been refreshed recently. This is primarily used
96 * in a locking scenario when some other process might have refreshed things underneath
97 * this process
98 *
99 * @param int $groupID the group ID
100 * @param boolean $includeHiddenGroups hidden groups are excluded by default
101 *
102 * @return string the sql query which lists the groups that need to be refreshed
103 * @static
104 * @public
105 */
4f53db5a 106 static function shouldGroupBeRefreshed($groupID, $includeHiddenGroups = FALSE) {
abdb2607
DL
107 $query = self::groupRefreshedClause("g.id = %1", $includeHiddenGroups);
108 $params = array(1 => array($groupID, 'Integer'));
109
4f53db5a 110 // if the query returns the group ID, it means the group is a valid candidate for refreshing
abdb2607
DL
111 return CRM_Core_DAO::singleValueQuery($query, $params);
112 }
113
6a488035
TO
114 /**
115 * Check to see if we have cache entries for this group
116 * if not, regenerate, else return
117 *
118 * @param int/array $groupID groupID of group that we are checking against
119 * if empty, all groups are checked
120 * @param int $limit limits the number of groups we evaluate
121 *
122 * @return boolean true if we did not regenerate, false if we did
123 */
124 static function loadAll($groupIDs = null, $limit = 0) {
125 // ensure that all the smart groups are loaded
126 // this function is expensive and should be sparingly used if groupIDs is empty
6a488035
TO
127 if (empty($groupIDs)) {
128 $groupIDClause = null;
129 $groupIDs = array( );
130 }
131 else {
132 if (!is_array($groupIDs)) {
133 $groupIDs = array($groupIDs);
134 }
135
136 // note escapeString is a must here and we can't send the imploded value as second arguement to
137 // the executeQuery(), since that would put single quote around the string and such a string
138 // of comma separated integers would not work.
0e4e5a49 139 $groupIDString = CRM_Core_DAO::escapeString(implode(', ', $groupIDs));
6a488035 140
abdb2607 141 $groupIDClause = "g.id IN ({$groupIDString})";
6a488035
TO
142 }
143
abdb2607 144 $query = self::groupRefreshedClause($groupIDClause);
6a488035
TO
145
146 $limitClause = $orderClause = NULL;
147 if ($limit > 0) {
148 $limitClause = " LIMIT 0, $limit";
149 $orderClause = " ORDER BY g.cache_date, g.refresh_date";
150 }
e2422b8f 151 // We ignore hidden groups and disabled groups
abdb2607 152 $query .= "
6a488035 153 $orderClause
f9e16e9a 154 $limitClause
6a488035
TO
155";
156
157 $dao = CRM_Core_DAO::executeQuery($query);
158 $processGroupIDs = array();
159 $refreshGroupIDs = $groupIDs;
160 while ($dao->fetch()) {
161 $processGroupIDs[] = $dao->id;
162
163 // remove this id from refreshGroupIDs
164 foreach ($refreshGroupIDs as $idx => $gid) {
165 if ($gid == $dao->id) {
166 unset($refreshGroupIDs[$idx]);
167 break;
168 }
169 }
170 }
171
172 if (!empty($refreshGroupIDs)) {
82a837e9 173 $refreshGroupIDString = CRM_Core_DAO::escapeString(implode(', ', $refreshGroupIDs));
634366e7 174 $time = CRM_Utils_Date::getUTCTime(self::smartGroupCacheTimeout() * 60);
6a488035
TO
175 $query = "
176UPDATE civicrm_group g
177SET g.refresh_date = $time
178WHERE g.id IN ( {$refreshGroupIDString} )
179AND g.refresh_date IS NULL
180";
82a837e9 181 CRM_Core_DAO::executeQuery($query);
6a488035
TO
182 }
183
184 if (empty($processGroupIDs)) {
185 return TRUE;
186 }
187 else {
188 self::add($processGroupIDs);
189 return FALSE;
190 }
191 }
192
86538308
EM
193 /**
194 * @param $groupID
195 */
6a488035
TO
196 static function add($groupID) {
197 // first delete the current cache
198 self::remove($groupID);
199 if (!is_array($groupID)) {
200 $groupID = array($groupID);
201 }
202
203 $returnProperties = array('contact_id');
204 foreach ($groupID as $gid) {
205 $params = array(array('group', 'IN', array($gid => 1), 0, 0));
abdb2607 206 // the below call updates the cache table as a byproduct of the query
6a488035
TO
207 CRM_Contact_BAO_Query::apiQuery($params, $returnProperties, NULL, NULL, 0, 0, FALSE);
208 }
209 }
210
86538308
EM
211 /**
212 * @param $groupID
213 * @param $values
214 */
6a488035
TO
215 static function store(&$groupID, &$values) {
216 $processed = FALSE;
217
218 // sort the values so we put group IDs in front and hence optimize
219 // mysql storage (or so we think) CRM-9493
220 sort($values);
eb917190 221
6a488035
TO
222 // to avoid long strings, lets do BULK_INSERT_COUNT values at a time
223 while (!empty($values)) {
224 $processed = TRUE;
225 $input = array_splice($values, 0, CRM_Core_DAO::BULK_INSERT_COUNT);
226 $str = implode(',', $input);
eb917190 227 $sql = "INSERT IGNORE INTO civicrm_group_contact_cache (group_id,contact_id) VALUES $str;";
6a488035
TO
228 CRM_Core_DAO::executeQuery($sql);
229 }
230 self::updateCacheTime($groupID, $processed);
231 }
232
233 /**
234 * Change the cache_date
235 *
236 * @param $groupID array(int)
237 * @param $processed bool, whether the cache data was recently modified
238 */
239 static function updateCacheTime($groupID, $processed) {
240 // only update cache entry if we had any values
241 if ($processed) {
242 // also update the group with cache date information
243 //make sure to give original timezone settings again.
244 $now = CRM_Utils_Date::getUTCTime();
245 $refresh = 'null';
246 }
247 else {
248 $now = 'null';
249 $refresh = 'null';
250 }
251
252 $groupIDs = implode(',', $groupID);
253 $sql = "
254UPDATE civicrm_group
255SET cache_date = $now, refresh_date = $refresh
256WHERE id IN ( $groupIDs )
257";
258 CRM_Core_DAO::executeQuery($sql);
259 }
260
cee4f6a1
DL
261 /**
262 * Removes all the cache entries pertaining to a specific group
263 * If no groupID is passed in, removes cache entries for all groups
264 * Has an optimization to bypass repeated invocations of this function.
265 * Note that this function is an advisory, i.e. the removal respects the
266 * cache date, i.e. the removal is not done if the group was recently
267 * loaded into the cache.
268 *
269 * @param $groupID int the groupID to delete cache entries, NULL for all groups
270 * @param $onceOnly boolean run the function exactly once for all groups.
271 *
272 * @public
273 * @return void
274 * @static
275 */
6a488035
TO
276 static function remove($groupID = NULL, $onceOnly = TRUE) {
277 static $invoked = FALSE;
278
279 // typically this needs to happy only once per instance
280 // this is especially true in import, where we dont need
281 // to do this all the time
282 // this optimization is done only when no groupID is passed
283 // i.e. cache is reset for all groups
abdb2607
DL
284 if (
285 $onceOnly &&
6a488035
TO
286 $invoked &&
287 $groupID == NULL
288 ) {
289 return;
290 }
291
292 if ($groupID == NULL) {
293 $invoked = TRUE;
294 } else if (is_array($groupID)) {
abdb2607 295 foreach ($groupID as $gid) {
6a488035 296 unset(self::$_alreadyLoaded[$gid]);
abdb2607 297 }
6a488035
TO
298 } else if ($groupID && array_key_exists($groupID, self::$_alreadyLoaded)) {
299 unset(self::$_alreadyLoaded[$groupID]);
300 }
301
302 $refresh = null;
303 $params = array();
304 $smartGroupCacheTimeout = self::smartGroupCacheTimeout();
305
306 $now = CRM_Utils_Date::getUTCTime();
cf142e47 307 $refreshTime = CRM_Utils_Date::getUTCTime($smartGroupCacheTimeout * 60);
6a488035
TO
308
309 if (!isset($groupID)) {
310 if ($smartGroupCacheTimeout == 0) {
311 $query = "
312TRUNCATE civicrm_group_contact_cache
313";
314 $update = "
315UPDATE civicrm_group g
316SET cache_date = null,
317 refresh_date = null
318";
319 }
320 else {
321 $query = "
322DELETE gc
323FROM civicrm_group_contact_cache gc
324INNER JOIN civicrm_group g ON g.id = gc.group_id
325WHERE TIMESTAMPDIFF(MINUTE, g.cache_date, $now) >= $smartGroupCacheTimeout
326";
327 $update = "
328UPDATE civicrm_group g
329SET cache_date = null,
330 refresh_date = null
331WHERE TIMESTAMPDIFF(MINUTE, cache_date, $now) >= $smartGroupCacheTimeout
332";
333 $refresh = "
334UPDATE civicrm_group g
335SET refresh_date = $refreshTime
336WHERE TIMESTAMPDIFF(MINUTE, cache_date, $now) < $smartGroupCacheTimeout
337AND refresh_date IS NULL
338";
339 }
340 }
341 elseif (is_array($groupID)) {
342 $groupIDs = implode(', ', $groupID);
343 $query = "
344DELETE g
345FROM civicrm_group_contact_cache g
346WHERE g.group_id IN ( $groupIDs )
347";
348 $update = "
349UPDATE civicrm_group g
350SET cache_date = null,
351 refresh_date = null
352WHERE id IN ( $groupIDs )
353";
354 }
355 else {
356 $query = "
357DELETE g
358FROM civicrm_group_contact_cache g
359WHERE g.group_id = %1
360";
361 $update = "
362UPDATE civicrm_group g
363SET cache_date = null,
364 refresh_date = null
365WHERE id = %1
366";
367 $params = array(1 => array($groupID, 'Integer'));
368 }
369
370 CRM_Core_DAO::executeQuery($query, $params);
371
372 if ($refresh) {
373 CRM_Core_DAO::executeQuery($refresh, $params);
374 }
375
376 // also update the cache_date for these groups
377 CRM_Core_DAO::executeQuery($update, $params);
378 }
379
380 /**
381 * load the smart group cache for a saved search
abdb2607
DL
382 *
383 * @param object $group - the smart group that needs to be loaded
384 * @param boolean $force - should we force a search through
385 *
6a488035 386 */
abdb2607 387 static function load(&$group, $force = FALSE) {
6a488035
TO
388 $groupID = $group->id;
389 $savedSearchID = $group->saved_search_id;
abdb2607 390 if (array_key_exists($groupID, self::$_alreadyLoaded) && !$force) {
6a488035
TO
391 return;
392 }
cc13551d
DL
393
394 // grab a lock so other processes dont compete and do the same query
395 $lockName = "civicrm.group.{$groupID}";
396 $lock = new CRM_Core_Lock($lockName);
397 if (!$lock->isAcquired()) {
398 // this can cause inconsistent results since we dont know if the other process
399 // will fill up the cache before our calling routine needs it.
400 // however this routine does not return the status either, so basically
401 // its a "lets return and hope for the best"
402 return;
403 }
404
6a488035 405 self::$_alreadyLoaded[$groupID] = 1;
abdb2607
DL
406
407 // we now have the lock, but some other proces could have actually done the work
408 // before we got here, so before we do any work, lets ensure that work needs to be
409 // done
410 // we allow hidden groups here since we dont know if the caller wants to evaluate an
411 // hidden group
3cd86345 412 if (!$force && !self::shouldGroupBeRefreshed($groupID, TRUE)) {
abdb2607
DL
413 $lock->release();
414 return;
415 }
416
6a488035
TO
417 $sql = NULL;
418 $idName = 'id';
419 $customClass = NULL;
420 if ($savedSearchID) {
421 $ssParams = CRM_Contact_BAO_SavedSearch::getSearchParams($savedSearchID);
422
423 // rectify params to what proximity search expects if there is a value for prox_distance
424 // CRM-7021
425 if (!empty($ssParams)) {
426 CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams);
427 }
428
429
430 $returnProperties = array();
431 if (CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $savedSearchID, 'mapping_id')) {
432 $fv = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
433 $returnProperties = CRM_Core_BAO_Mapping::returnProperties($fv);
434 }
435
436 if (isset($ssParams['customSearchID'])) {
437 // if custom search
438
439 // we split it up and store custom class
440 // so temp tables are not destroyed if they are used
441 // hence customClass is defined above at top of function
442 $customClass =
443 CRM_Contact_BAO_SearchCustom::customClass($ssParams['customSearchID'], $savedSearchID);
444 $searchSQL = $customClass->contactIDs();
26a9b6ab 445 $searchSQL = str_replace('ORDER BY contact_a.id ASC', '', $searchSQL);
6a488035
TO
446 $idName = 'contact_id';
447 }
448 else {
449 $formValues = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
450
451 $query =
452 new CRM_Contact_BAO_Query(
453 $ssParams, $returnProperties, NULL,
cc13551d
DL
454 FALSE, FALSE, 1,
455 TRUE, TRUE,
456 FALSE,
6a488035
TO
457 CRM_Utils_Array::value('display_relationship_type', $formValues),
458 CRM_Utils_Array::value('operator', $formValues, 'AND')
459 );
460 $query->_useDistinct = FALSE;
461 $query->_useGroupBy = FALSE;
462 $searchSQL =
463 $query->searchQuery(
464 0, 0, NULL,
465 FALSE, FALSE,
466 FALSE, TRUE,
467 TRUE,
468 NULL, NULL, NULL,
469 TRUE
470 );
471 }
472 $groupID = CRM_Utils_Type::escape($groupID, 'Integer');
473 $sql = $searchSQL . " AND contact_a.id NOT IN (
474 SELECT contact_id FROM civicrm_group_contact
475 WHERE civicrm_group_contact.status = 'Removed'
476 AND civicrm_group_contact.group_id = $groupID ) ";
477 }
478
479 if ($sql) {
480 $sql = preg_replace("/^\s*SELECT/", "SELECT $groupID as group_id, ", $sql);
481 }
482
483 // lets also store the records that are explicitly added to the group
484 // this allows us to skip the group contact LEFT JOIN
485 $sqlB = "
486SELECT $groupID as group_id, contact_id as $idName
487FROM civicrm_group_contact
488WHERE civicrm_group_contact.status = 'Added'
489 AND civicrm_group_contact.group_id = $groupID ";
490
491 $groupIDs = array($groupID);
492 self::remove($groupIDs);
abdb2607 493 $processed = FALSE;
b8eae4bd 494 $tempTable = 'civicrm_temp_group_contact_cache' . rand(0,2000);
6a488035
TO
495 foreach (array($sql, $sqlB) as $selectSql) {
496 if (!$selectSql) {
497 continue;
498 }
b8eae4bd 499 $insertSql = "CREATE TEMPORARY TABLE $tempTable ($selectSql);";
eb917190 500 $processed = TRUE;
6a488035 501 $result = CRM_Core_DAO::executeQuery($insertSql);
b8eae4bd 502 CRM_Core_DAO::executeQuery(
503 "INSERT IGNORE INTO civicrm_group_contact_cache (contact_id, group_id)
eab13399 504 SELECT DISTINCT $idName, group_id FROM $tempTable
b8eae4bd 505 ");
506 CRM_Core_DAO::executeQuery(" DROP TABLE $tempTable");
6a488035 507 }
b8eae4bd 508
6a488035
TO
509 self::updateCacheTime($groupIDs, $processed);
510
511 if ($group->children) {
512
513 //Store a list of contacts who are removed from the parent group
514 $sql = "
515SELECT contact_id
516FROM civicrm_group_contact
517WHERE civicrm_group_contact.status = 'Removed'
518AND civicrm_group_contact.group_id = $groupID ";
519 $dao = CRM_Core_DAO::executeQuery($sql);
520 $removed_contacts = array();
521 while ($dao->fetch()) {
522 $removed_contacts[] = $dao->contact_id;
523 }
524
525 $childrenIDs = explode(',', $group->children);
526 foreach ($childrenIDs as $childID) {
527 $contactIDs = CRM_Contact_BAO_Group::getMember($childID, FALSE);
528 //Unset each contact that is removed from the parent group
529 foreach ($removed_contacts as $removed_contact) {
530 unset($contactIDs[$removed_contact]);
531 }
532 $values = array();
533 foreach ($contactIDs as $contactID => $dontCare) {
534 $values[] = "({$groupID},{$contactID})";
535 }
536
537 self::store($groupIDs, $values);
538 }
539 }
cc13551d
DL
540
541 $lock->release();
6a488035
TO
542 }
543
86538308
EM
544 /**
545 * @return int
546 */
6a488035
TO
547 static function smartGroupCacheTimeout() {
548 $config = CRM_Core_Config::singleton();
549
550 if (
551 isset($config->smartGroupCacheTimeout) &&
552 is_numeric($config->smartGroupCacheTimeout) &&
553 $config->smartGroupCacheTimeout > 0) {
554 return $config->smartGroupCacheTimeout;
555 }
556
557 // lets have a min cache time of 5 mins if not set
558 return 5;
559 }
560
9be31c7a
DL
561 /**
562 * Get all the smart groups that this contact belongs to
563 * Note that this could potentially be a super slow function since
564 * it ensure that all contact groups are loaded in the cache
565 *
566 * @param int $contactID
567 * @param boolean $showHidden - hidden groups are shown only if this flag is set
568 *
569 * @return array an array of groups that this contact belongs to
570 */
571 static function contactGroup($contactID, $showHidden = FALSE) {
6a488035
TO
572 if (empty($contactID)) {
573 return;
574 }
575
576 if (is_array($contactID)) {
577 $contactIDs = $contactID;
578 }
579 else {
580 $contactIDs = array($contactID);
581 }
582
583 self::loadAll();
584
9be31c7a
DL
585 $hiddenClause = '';
586 if (!$showHidden) {
587 $hiddenClause = ' AND (g.is_hidden = 0 OR g.is_hidden IS NULL) ';
588 }
589
6a488035
TO
590 $contactIDString = CRM_Core_DAO::escapeString(implode(', ', $contactIDs));
591 $sql = "
592SELECT gc.group_id, gc.contact_id, g.title, g.children, g.description
593FROM civicrm_group_contact_cache gc
594INNER JOIN civicrm_group g ON g.id = gc.group_id
595WHERE gc.contact_id IN ($contactIDString)
9be31c7a 596 $hiddenClause
6a488035
TO
597ORDER BY gc.contact_id, g.children
598";
599
600 $dao = CRM_Core_DAO::executeQuery($sql);
601 $contactGroup = array();
602 $prevContactID = null;
603 while ($dao->fetch()) {
604 if (
605 $prevContactID &&
606 $prevContactID != $dao->contact_id
607 ) {
608 $contactGroup[$prevContactID]['groupTitle'] = implode(', ', $contactGroup[$prevContactID]['groupTitle']);
609 }
610 $prevContactID = $dao->contact_id;
611 if (!array_key_exists($dao->contact_id, $contactGroup)) {
612 $contactGroup[$dao->contact_id] =
613 array( 'group' => array(), 'groupTitle' => array());
614 }
615
616 $contactGroup[$dao->contact_id]['group'][] =
617 array(
618 'id' => $dao->group_id,
619 'title' => $dao->title,
620 'description' => $dao->description,
621 'children' => $dao->children
622 );
623 $contactGroup[$dao->contact_id]['groupTitle'][] = $dao->title;
624 }
625
626 if ($prevContactID) {
627 $contactGroup[$prevContactID]['groupTitle'] = implode(', ', $contactGroup[$prevContactID]['groupTitle']);
628 }
629
13982f7b 630 if ((!empty($contactGroup[$contactID]) && is_numeric($contactID))) {
6a488035
TO
631 return $contactGroup[$contactID];
632 }
633 else {
634 return $contactGroup;
635 }
636 }
637
638}
639