Merge pull request #651 from demeritcowboy/misc4.3
[civicrm-core.git] / CRM / Contact / BAO / GroupContactCache.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.3 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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
31 * @copyright CiviCRM LLC (c) 2004-2013
32 * $Id$
33 *
34 */
35 class 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 *
43 * @param int $groupID groupID of group that we are checking against
44 *
45 * @return boolean true if we did not regenerate, false if we did
46 */
47 static function check($groupIDs) {
48 if (empty($groupIDs)) {
49 return TRUE;
50 }
51
52 return self::loadAll($groupIDs);
53 }
54
55 /**
56 * Check to see if we have cache entries for this group
57 * if not, regenerate, else return
58 *
59 * @param int/array $groupID groupID of group that we are checking against
60 * if empty, all groups are checked
61 * @param int $limit limits the number of groups we evaluate
62 *
63 * @return boolean true if we did not regenerate, false if we did
64 */
65 static function loadAll($groupIDs = null, $limit = 0) {
66 // ensure that all the smart groups are loaded
67 // this function is expensive and should be sparingly used if groupIDs is empty
68
69 if (empty($groupIDs)) {
70 $groupIDClause = null;
71 $groupIDs = array( );
72 }
73 else {
74 if (!is_array($groupIDs)) {
75 $groupIDs = array($groupIDs);
76 }
77
78 // note escapeString is a must here and we can't send the imploded value as second arguement to
79 // the executeQuery(), since that would put single quote around the string and such a string
80 // of comma separated integers would not work.
81 $groupIDString = CRM_Core_DAO::escapeString(implode(', ', $groupIDs));
82
83 $groupIDClause = "AND (g.id IN ( {$groupIDString} ))";
84 }
85
86 $smartGroupCacheTimeout = self::smartGroupCacheTimeout();
87
88 //make sure to give original timezone settings again.
89 $now = CRM_Utils_Date::getUTCTime();
90
91 $limitClause = $orderClause = NULL;
92 if ($limit > 0) {
93 $limitClause = " LIMIT 0, $limit";
94 $orderClause = " ORDER BY g.cache_date, g.refresh_date";
95 }
96 $query = "
97 SELECT g.id
98 FROM civicrm_group g
99 WHERE ( g.saved_search_id IS NOT NULL OR g.children IS NOT NULL )
100 AND ( g.is_hidden = 0 OR g.is_hidden IS NULL )
101 AND ( g.cache_date IS NULL OR
102 ( TIMESTAMPDIFF(MINUTE, g.cache_date, $now) >= $smartGroupCacheTimeout ) OR
103 ( $now >= g.refresh_date )
104 )
105 $groupIDClause
106 $orderClause
107 $limitClause
108 ";
109
110 $dao = CRM_Core_DAO::executeQuery($query);
111 $processGroupIDs = array();
112 $refreshGroupIDs = $groupIDs;
113 while ($dao->fetch()) {
114 $processGroupIDs[] = $dao->id;
115
116 // remove this id from refreshGroupIDs
117 foreach ($refreshGroupIDs as $idx => $gid) {
118 if ($gid == $dao->id) {
119 unset($refreshGroupIDs[$idx]);
120 break;
121 }
122 }
123 }
124
125 if (!empty($refreshGroupIDs)) {
126 $refreshGroupIDString = CRM_Core_DAO::escapeString(implode(', ', $refreshGroupIDs));
127 $time = CRM_Utils_Date::getUTCTime($smartGroupCacheTimeout * 60);
128 $query = "
129 UPDATE civicrm_group g
130 SET g.refresh_date = $time
131 WHERE g.id IN ( {$refreshGroupIDString} )
132 AND g.refresh_date IS NULL
133 ";
134 CRM_Core_DAO::executeQuery($query);
135 }
136
137 if (empty($processGroupIDs)) {
138 return TRUE;
139 }
140 else {
141 self::add($processGroupIDs);
142 return FALSE;
143 }
144 }
145
146 static function add($groupID) {
147 // first delete the current cache
148 self::remove($groupID);
149 if (!is_array($groupID)) {
150 $groupID = array($groupID);
151 }
152
153 $returnProperties = array('contact_id');
154 foreach ($groupID as $gid) {
155 $params = array(array('group', 'IN', array($gid => 1), 0, 0));
156 // the below call update the cache table as a byproduct of the query
157 CRM_Contact_BAO_Query::apiQuery($params, $returnProperties, NULL, NULL, 0, 0, FALSE);
158 }
159 }
160
161 static function store(&$groupID, &$values) {
162 $processed = FALSE;
163
164 // sort the values so we put group IDs in front and hence optimize
165 // mysql storage (or so we think) CRM-9493
166 sort($values);
167
168 // to avoid long strings, lets do BULK_INSERT_COUNT values at a time
169 while (!empty($values)) {
170 $processed = TRUE;
171 $input = array_splice($values, 0, CRM_Core_DAO::BULK_INSERT_COUNT);
172 $str = implode(',', $input);
173 $sql = "INSERT IGNORE INTO civicrm_group_contact_cache (group_id,contact_id) VALUES $str;";
174 CRM_Core_DAO::executeQuery($sql);
175 }
176 self::updateCacheTime($groupID, $processed);
177 }
178
179 /**
180 * Change the cache_date
181 *
182 * @param $groupID array(int)
183 * @param $processed bool, whether the cache data was recently modified
184 */
185 static function updateCacheTime($groupID, $processed) {
186 // only update cache entry if we had any values
187 if ($processed) {
188 // also update the group with cache date information
189 //make sure to give original timezone settings again.
190 $now = CRM_Utils_Date::getUTCTime();
191 $refresh = 'null';
192 }
193 else {
194 $now = 'null';
195 $refresh = 'null';
196 }
197
198 $groupIDs = implode(',', $groupID);
199 $sql = "
200 UPDATE civicrm_group
201 SET cache_date = $now, refresh_date = $refresh
202 WHERE id IN ( $groupIDs )
203 ";
204 CRM_Core_DAO::executeQuery($sql);
205 }
206
207 static function remove($groupID = NULL, $onceOnly = TRUE) {
208 static $invoked = FALSE;
209
210 // typically this needs to happy only once per instance
211 // this is especially true in import, where we dont need
212 // to do this all the time
213 // this optimization is done only when no groupID is passed
214 // i.e. cache is reset for all groups
215 if ($onceOnly &&
216 $invoked &&
217 $groupID == NULL
218 ) {
219 return;
220 }
221
222 if ($groupID == NULL) {
223 $invoked = TRUE;
224 } else if (is_array($groupID)) {
225 foreach ($groupID as $gid)
226 unset(self::$_alreadyLoaded[$gid]);
227 } else if ($groupID && array_key_exists($groupID, self::$_alreadyLoaded)) {
228 unset(self::$_alreadyLoaded[$groupID]);
229 }
230
231 $refresh = null;
232 $params = array();
233 $smartGroupCacheTimeout = self::smartGroupCacheTimeout();
234
235 $now = CRM_Utils_Date::getUTCTime();
236 $refreshTime = CRM_Utils_Date::getUTCTime($smartGroupCacheTimeout * 60);
237
238 if (!isset($groupID)) {
239 if ($smartGroupCacheTimeout == 0) {
240 $query = "
241 TRUNCATE civicrm_group_contact_cache
242 ";
243 $update = "
244 UPDATE civicrm_group g
245 SET cache_date = null,
246 refresh_date = null
247 ";
248 }
249 else {
250 $query = "
251 DELETE gc
252 FROM civicrm_group_contact_cache gc
253 INNER JOIN civicrm_group g ON g.id = gc.group_id
254 WHERE TIMESTAMPDIFF(MINUTE, g.cache_date, $now) >= $smartGroupCacheTimeout
255 ";
256 $update = "
257 UPDATE civicrm_group g
258 SET cache_date = null,
259 refresh_date = null
260 WHERE TIMESTAMPDIFF(MINUTE, cache_date, $now) >= $smartGroupCacheTimeout
261 ";
262 $refresh = "
263 UPDATE civicrm_group g
264 SET refresh_date = $refreshTime
265 WHERE TIMESTAMPDIFF(MINUTE, cache_date, $now) < $smartGroupCacheTimeout
266 AND refresh_date IS NULL
267 ";
268 }
269 }
270 elseif (is_array($groupID)) {
271 $groupIDs = implode(', ', $groupID);
272 $query = "
273 DELETE g
274 FROM civicrm_group_contact_cache g
275 WHERE g.group_id IN ( $groupIDs )
276 ";
277 $update = "
278 UPDATE civicrm_group g
279 SET cache_date = null,
280 refresh_date = null
281 WHERE id IN ( $groupIDs )
282 ";
283 }
284 else {
285 $query = "
286 DELETE g
287 FROM civicrm_group_contact_cache g
288 WHERE g.group_id = %1
289 ";
290 $update = "
291 UPDATE civicrm_group g
292 SET cache_date = null,
293 refresh_date = null
294 WHERE id = %1
295 ";
296 $params = array(1 => array($groupID, 'Integer'));
297 }
298
299 CRM_Core_DAO::executeQuery($query, $params);
300
301 if ($refresh) {
302 CRM_Core_DAO::executeQuery($refresh, $params);
303 }
304
305 // also update the cache_date for these groups
306 CRM_Core_DAO::executeQuery($update, $params);
307 }
308
309 /**
310 * load the smart group cache for a saved search
311 */
312 static function load(&$group, $fresh = FALSE) {
313 $groupID = $group->id;
314 $savedSearchID = $group->saved_search_id;
315 if (array_key_exists($groupID, self::$_alreadyLoaded) && !$fresh) {
316 return;
317 }
318 self::$_alreadyLoaded[$groupID] = 1;
319 $sql = NULL;
320 $idName = 'id';
321 $customClass = NULL;
322 if ($savedSearchID) {
323 $ssParams = CRM_Contact_BAO_SavedSearch::getSearchParams($savedSearchID);
324
325 // rectify params to what proximity search expects if there is a value for prox_distance
326 // CRM-7021
327 if (!empty($ssParams)) {
328 CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams);
329 }
330
331
332 $returnProperties = array();
333 if (CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $savedSearchID, 'mapping_id')) {
334 $fv = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
335 $returnProperties = CRM_Core_BAO_Mapping::returnProperties($fv);
336 }
337
338 if (isset($ssParams['customSearchID'])) {
339 // if custom search
340
341 // we split it up and store custom class
342 // so temp tables are not destroyed if they are used
343 // hence customClass is defined above at top of function
344 $customClass =
345 CRM_Contact_BAO_SearchCustom::customClass($ssParams['customSearchID'], $savedSearchID);
346 $searchSQL = $customClass->contactIDs();
347 $idName = 'contact_id';
348 }
349 else {
350 $formValues = CRM_Contact_BAO_SavedSearch::getFormValues($savedSearchID);
351
352 $query =
353 new CRM_Contact_BAO_Query(
354 $ssParams, $returnProperties, NULL,
355 FALSE, FALSE, 1,
356 TRUE, TRUE,
357 FALSE,
358 CRM_Utils_Array::value('display_relationship_type', $formValues),
359 CRM_Utils_Array::value('operator', $formValues, 'AND')
360 );
361 $query->_useDistinct = FALSE;
362 $query->_useGroupBy = FALSE;
363 $searchSQL =
364 $query->searchQuery(
365 0, 0, NULL,
366 FALSE, FALSE,
367 FALSE, TRUE,
368 TRUE,
369 NULL, NULL, NULL,
370 TRUE
371 );
372 }
373 $groupID = CRM_Utils_Type::escape($groupID, 'Integer');
374 $sql = $searchSQL . " AND contact_a.id NOT IN (
375 SELECT contact_id FROM civicrm_group_contact
376 WHERE civicrm_group_contact.status = 'Removed'
377 AND civicrm_group_contact.group_id = $groupID ) ";
378 }
379
380 if ($sql) {
381 $sql = preg_replace("/^\s*SELECT/", "SELECT $groupID as group_id, ", $sql);
382 }
383
384 // lets also store the records that are explicitly added to the group
385 // this allows us to skip the group contact LEFT JOIN
386 $sqlB = "
387 SELECT $groupID as group_id, contact_id as $idName
388 FROM civicrm_group_contact
389 WHERE civicrm_group_contact.status = 'Added'
390 AND civicrm_group_contact.group_id = $groupID ";
391
392 $groupIDs = array($groupID);
393 self::remove($groupIDs);
394
395 foreach (array($sql, $sqlB) as $selectSql) {
396 if (!$selectSql) {
397 continue;
398 }
399 $insertSql = "INSERT IGNORE INTO civicrm_group_contact_cache (group_id,contact_id) ($selectSql);";
400 $processed = TRUE; // FIXME
401 $result = CRM_Core_DAO::executeQuery($insertSql);
402 }
403 self::updateCacheTime($groupIDs, $processed);
404
405 if ($group->children) {
406
407 //Store a list of contacts who are removed from the parent group
408 $sql = "
409 SELECT contact_id
410 FROM civicrm_group_contact
411 WHERE civicrm_group_contact.status = 'Removed'
412 AND civicrm_group_contact.group_id = $groupID ";
413 $dao = CRM_Core_DAO::executeQuery($sql);
414 $removed_contacts = array();
415 while ($dao->fetch()) {
416 $removed_contacts[] = $dao->contact_id;
417 }
418
419 $childrenIDs = explode(',', $group->children);
420 foreach ($childrenIDs as $childID) {
421 $contactIDs = CRM_Contact_BAO_Group::getMember($childID, FALSE);
422 //Unset each contact that is removed from the parent group
423 foreach ($removed_contacts as $removed_contact) {
424 unset($contactIDs[$removed_contact]);
425 }
426 $values = array();
427 foreach ($contactIDs as $contactID => $dontCare) {
428 $values[] = "({$groupID},{$contactID})";
429 }
430
431 self::store($groupIDs, $values);
432 }
433 }
434 }
435
436 static function smartGroupCacheTimeout() {
437 $config = CRM_Core_Config::singleton();
438
439 if (
440 isset($config->smartGroupCacheTimeout) &&
441 is_numeric($config->smartGroupCacheTimeout) &&
442 $config->smartGroupCacheTimeout > 0) {
443 return $config->smartGroupCacheTimeout;
444 }
445
446 // lets have a min cache time of 5 mins if not set
447 return 5;
448 }
449
450 /**
451 * Get all the smart groups that this contact belongs to
452 * Note that this could potentially be a super slow function since
453 * it ensure that all contact groups are loaded in the cache
454 *
455 * @param int $contactID
456 * @param boolean $showHidden - hidden groups are shown only if this flag is set
457 *
458 * @return array an array of groups that this contact belongs to
459 */
460 static function contactGroup($contactID, $showHidden = FALSE) {
461 if (empty($contactID)) {
462 return;
463 }
464
465 if (is_array($contactID)) {
466 $contactIDs = $contactID;
467 }
468 else {
469 $contactIDs = array($contactID);
470 }
471
472 self::loadAll();
473
474 $hiddenClause = '';
475 if (!$showHidden) {
476 $hiddenClause = ' AND (g.is_hidden = 0 OR g.is_hidden IS NULL) ';
477 }
478
479 $contactIDString = CRM_Core_DAO::escapeString(implode(', ', $contactIDs));
480 $sql = "
481 SELECT gc.group_id, gc.contact_id, g.title, g.children, g.description
482 FROM civicrm_group_contact_cache gc
483 INNER JOIN civicrm_group g ON g.id = gc.group_id
484 WHERE gc.contact_id IN ($contactIDString)
485 $hiddenClause
486 ORDER BY gc.contact_id, g.children
487 ";
488
489 $dao = CRM_Core_DAO::executeQuery($sql);
490 $contactGroup = array();
491 $prevContactID = null;
492 while ($dao->fetch()) {
493 if (
494 $prevContactID &&
495 $prevContactID != $dao->contact_id
496 ) {
497 $contactGroup[$prevContactID]['groupTitle'] = implode(', ', $contactGroup[$prevContactID]['groupTitle']);
498 }
499 $prevContactID = $dao->contact_id;
500 if (!array_key_exists($dao->contact_id, $contactGroup)) {
501 $contactGroup[$dao->contact_id] =
502 array( 'group' => array(), 'groupTitle' => array());
503 }
504
505 $contactGroup[$dao->contact_id]['group'][] =
506 array(
507 'id' => $dao->group_id,
508 'title' => $dao->title,
509 'description' => $dao->description,
510 'children' => $dao->children
511 );
512 $contactGroup[$dao->contact_id]['groupTitle'][] = $dao->title;
513 }
514
515 if ($prevContactID) {
516 $contactGroup[$prevContactID]['groupTitle'] = implode(', ', $contactGroup[$prevContactID]['groupTitle']);
517 }
518
519 if (is_numeric($contactID)) {
520 return $contactGroup[$contactID];
521 }
522 else {
523 return $contactGroup;
524 }
525 }
526
527 }
528