f4f4aed558e32ead6deda58c46e2b7d3e0ef97b2
[civicrm-core.git] / CRM / Contact / Form / Search / Custom / Group.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
32 * $Id$
33 *
34 */
35 class CRM_Contact_Form_Search_Custom_Group
36 extends CRM_Contact_Form_Search_Custom_Base
37 implements CRM_Contact_Form_Search_Interface {
38
39 protected $_formValues;
40
41 protected $_tableName = NULL;
42
43 protected $_where = ' (1) ';
44
45 protected $_aclFrom = NULL;
46 protected $_aclWhere = NULL;
47
48 /**
49 * @param $formValues
50 */
51 function __construct(&$formValues) {
52 $this->_formValues = $formValues;
53 $this->_columns = array(
54 ts('Contact Id') => 'contact_id',
55 ts('Contact Type') => 'contact_type',
56 ts('Name') => 'sort_name',
57 ts('Group Name') => 'gname',
58 ts('Tag Name') => 'tname',
59 );
60
61 $this->_includeGroups = CRM_Utils_Array::value('includeGroups', $this->_formValues, array());
62 $this->_excludeGroups = CRM_Utils_Array::value('excludeGroups', $this->_formValues, array());
63 $this->_includeTags = CRM_Utils_Array::value('includeTags', $this->_formValues, array());
64 $this->_excludeTags = CRM_Utils_Array::value('excludeTags', $this->_formValues, array());
65
66 //define variables
67 $this->_allSearch = FALSE;
68 $this->_groups = FALSE;
69 $this->_tags = FALSE;
70 $this->_andOr = CRM_Utils_Array::value('andOr', $this->_formValues);
71
72
73 //make easy to check conditions for groups and tags are
74 //selected or it is empty search
75 if (empty($this->_includeGroups) && empty($this->_excludeGroups) &&
76 empty($this->_includeTags) && empty($this->_excludeTags)
77 ) {
78 //empty search
79 $this->_allSearch = TRUE;
80 }
81
82 $this->_groups = (!empty($this->_includeGroups) || !empty($this->_excludeGroups));
83
84 $this->_tags = (!empty($this->_includeTags) || !empty($this->_excludeTags));
85 }
86
87 function __destruct() {
88 // mysql drops the tables when connectiomn is terminated
89 // cannot drop tables here, since the search might be used
90 // in other parts after the object is destroyed
91 }
92
93 /**
94 * @param $form
95 */
96 function buildForm(&$form) {
97
98 $this->setTitle(ts('Include / Exclude Search'));
99
100 $groups = CRM_Core_PseudoConstant::group();
101
102 $tags = CRM_Core_PseudoConstant::get('CRM_Core_DAO_EntityTag', 'tag_id', array('onlyActive' => FALSE));
103 if (count($groups) == 0 || count($tags) == 0) {
104 CRM_Core_Session::setStatus(ts("At least one Group and Tag must be present for Custom Group / Tag search."), ts('Missing Group/Tag'));
105 $url = CRM_Utils_System::url('civicrm/contact/search/custom/list', 'reset=1');
106 CRM_Utils_System::redirect($url);
107 }
108
109 $inG = &$form->addElement('advmultiselect', 'includeGroups',
110 ts('Include Group(s)') . ' ', $groups,
111 array(
112 'size' => 5,
113 'style' => 'width:240px',
114 'class' => 'advmultiselect',
115 )
116 );
117
118 $outG = &$form->addElement('advmultiselect', 'excludeGroups',
119 ts('Exclude Group(s)') . ' ', $groups,
120 array(
121 'size' => 5,
122 'style' => 'width:240px',
123 'class' => 'advmultiselect',
124 )
125 );
126
127 $andOr = array(
128 '1' => ts('Show contacts that meet the Groups criteria AND the Tags criteria'),
129 '0' => ts('Show contacts that meet the Groups criteria OR the Tags criteria'),
130 );
131 $form->addRadio('andOr', ts('AND/OR'), $andOr, NULL, '<br />', TRUE);
132
133 $int = &$form->addElement('advmultiselect', 'includeTags',
134 ts('Include Tag(s)') . ' ', $tags,
135 array(
136 'size' => 5,
137 'style' => 'width:240px',
138 'class' => 'advmultiselect',
139 )
140 );
141
142 $outt = &$form->addElement('advmultiselect', 'excludeTags',
143 ts('Exclude Tag(s)') . ' ', $tags,
144 array(
145 'size' => 5,
146 'style' => 'width:240px',
147 'class' => 'advmultiselect',
148 )
149 );
150
151 //add/remove buttons for groups
152 $inG->setButtonAttributes('add', array('value' => ts('Add >>')));;
153 $outG->setButtonAttributes('add', array('value' => ts('Add >>')));;
154 $inG->setButtonAttributes('remove', array('value' => ts('<< Remove')));;
155 $outG->setButtonAttributes('remove', array('value' => ts('<< Remove')));;
156
157 //add/remove buttons for tags
158 $int->setButtonAttributes('add', array('value' => ts('Add >>')));;
159 $outt->setButtonAttributes('add', array('value' => ts('Add >>')));;
160 $int->setButtonAttributes('remove', array('value' => ts('<< Remove')));;
161 $outt->setButtonAttributes('remove', array('value' => ts('<< Remove')));;
162
163 /**
164 * if you are using the standard template, this array tells the template what elements
165 * are part of the search criteria
166 */
167 $form->assign('elements', array('includeGroups', 'excludeGroups', 'andOr', 'includeTags', 'excludeTags'));
168 }
169
170 /*
171 * Set search form field defaults here.
172 */
173 /**
174 * @return array
175 */
176 function setDefaultValues() {
177 $defaults = array( 'andOr' => '1' );
178
179 if (!empty($this->_formValues)) {
180 $defaults['andOr'] = CRM_Utils_Array::value('andOr', $this->_formValues, '1');
181
182 $defaults['includeGroups'] = CRM_Utils_Array::value('includeGroups', $this->_formValues);
183 $defaults['excludeGroups'] = CRM_Utils_Array::value('excludeGroups', $this->_formValues);
184 }
185
186 return $defaults;
187 }
188
189 /**
190 * @param int $offset
191 * @param int $rowcount
192 * @param null $sort
193 * @param bool $includeContactIDs
194 * @param bool $justIDs
195 *
196 * @return string
197 */
198 function all(
199 $offset = 0, $rowcount = 0, $sort = NULL,
200 $includeContactIDs = FALSE, $justIDs = FALSE
201 ) {
202
203 if ($justIDs) {
204 $selectClause = "contact_a.id as contact_id";
205 }
206 else {
207 $selectClause = "contact_a.id as contact_id,
208 contact_a.contact_type as contact_type,
209 contact_a.sort_name as sort_name";
210
211 //distinguish column according to user selection
212 if (($this->_includeGroups && !$this->_includeTags)) {
213 unset($this->_columns['Tag Name']);
214 $selectClause .= ", GROUP_CONCAT(DISTINCT group_names ORDER BY group_names ASC ) as gname";
215 }
216 elseif ($this->_includeTags && (!$this->_includeGroups)) {
217 unset($this->_columns['Group Name']);
218 $selectClause .= ", GROUP_CONCAT(DISTINCT tag_names ORDER BY tag_names ASC ) as tname";
219 }
220 elseif (!empty($this->_includeTags) && !empty($this->_includeGroups)) {
221 $selectClause .= ", GROUP_CONCAT(DISTINCT group_names ORDER BY group_names ASC ) as gname , GROUP_CONCAT(DISTINCT tag_names ORDER BY tag_names ASC ) as tname";
222 }
223 else {
224 unset($this->_columns['Tag Name']);
225 unset($this->_columns['Group Name']);
226 }
227 }
228
229 $from = $this->from();
230
231 $where = $this->where($includeContactIDs);
232
233 if (!$justIDs && !$this->_allSearch) {
234 $groupBy = " GROUP BY contact_a.id";
235 }
236 else {
237 // CRM-10850
238 // we do this since this if stmt is called by the smart group part of the code
239 // adding a groupBy clause and saving it as a smart group messes up the query and
240 // bad things happen
241 // andrew hunt seemed to have rewritten this piece when he worked on this search
242 $groupBy = null;
243 }
244
245 $sql = "SELECT $selectClause $from WHERE $where $groupBy";
246
247 // Define ORDER BY for query in $sort, with default value
248 if (!$justIDs) {
249 if (!empty($sort)) {
250 if (is_string($sort)) {
251 $sort = CRM_Utils_Type::escape($sort, 'String');
252 $sql .= " ORDER BY $sort ";
253 }
254 else {
255 $sql .= " ORDER BY " . trim($sort->orderBy());
256 }
257 }
258 else {
259 $sql .= " ORDER BY contact_id ASC";
260 }
261 }
262 else {
263 $sql .= " ORDER BY contact_a.id ASC";
264 }
265
266 if ($offset >= 0 && $rowcount > 0) {
267 $sql .= " LIMIT $offset, $rowcount ";
268 }
269
270 return $sql;
271 }
272
273 /**
274 * @return string
275 * @throws Exception
276 */
277 function from() {
278
279 $iGroups = $xGroups = $iTags = $xTags = 0;
280
281 //define table name
282 $randomNum = md5(uniqid());
283 $this->_tableName = "civicrm_temp_custom_{$randomNum}";
284
285 //block for Group search
286 $smartGroup = array();
287 if ($this->_groups || $this->_allSearch) {
288 $group = new CRM_Contact_DAO_Group();
289 $group->is_active = 1;
290 $group->find();
291 while ($group->fetch()) {
292 $allGroups[] = $group->id;
293 if ($group->saved_search_id) {
294 $smartGroup[$group->saved_search_id] = $group->id;
295 }
296 }
297 $includedGroups = implode(',', $allGroups);
298
299 if (!empty($this->_includeGroups)) {
300 $iGroups = implode(',', $this->_includeGroups);
301 }
302 else {
303 //if no group selected search for all groups
304 $iGroups = NULL;
305 }
306 if (is_array($this->_excludeGroups)) {
307 $xGroups = implode(',', $this->_excludeGroups);
308 }
309 else {
310 $xGroups = 0;
311 }
312
313 $sql = "CREATE TEMPORARY TABLE Xg_{$this->_tableName} ( contact_id int primary key) ENGINE=MyISAM";
314 CRM_Core_DAO::executeQuery($sql);
315
316 //used only when exclude group is selected
317 if ($xGroups != 0) {
318 $excludeGroup = "INSERT INTO Xg_{$this->_tableName} ( contact_id )
319 SELECT DISTINCT civicrm_group_contact.contact_id
320 FROM civicrm_group_contact, civicrm_contact
321 WHERE
322 civicrm_contact.id = civicrm_group_contact.contact_id AND
323 civicrm_group_contact.status = 'Added' AND
324 civicrm_group_contact.group_id IN( {$xGroups})";
325
326 CRM_Core_DAO::executeQuery($excludeGroup);
327
328 //search for smart group contacts
329 foreach ($this->_excludeGroups as $keys => $values) {
330 if (in_array($values, $smartGroup)) {
331 $ssGroup = new CRM_Contact_DAO_Group();
332 $ssGroup->id = $values;
333 if (!$ssGroup->find(TRUE)) {
334 CRM_Core_Error::fatal();
335 }
336 CRM_Contact_BAO_GroupContactCache::load($ssGroup);
337
338 $smartSql = "
339 SELECT gcc.contact_id
340 FROM civicrm_group_contact_cache gcc
341 WHERE gcc.group_id = {$ssGroup->id}
342 ";
343 $smartGroupQuery = " INSERT IGNORE INTO Xg_{$this->_tableName}(contact_id) $smartSql";
344 CRM_Core_DAO::executeQuery($smartGroupQuery);
345 }
346 }
347 }
348
349 $sql = "CREATE TEMPORARY TABLE Ig_{$this->_tableName} ( id int PRIMARY KEY AUTO_INCREMENT,
350 contact_id int,
351 group_names varchar(64)) ENGINE=MyISAM";
352
353 CRM_Core_DAO::executeQuery($sql);
354
355 if ($iGroups) {
356 $includeGroup = "INSERT INTO Ig_{$this->_tableName} (contact_id, group_names)
357 SELECT civicrm_contact.id as contact_id, civicrm_group.title as group_name
358 FROM civicrm_contact
359 INNER JOIN civicrm_group_contact
360 ON civicrm_group_contact.contact_id = civicrm_contact.id
361 LEFT JOIN civicrm_group
362 ON civicrm_group_contact.group_id = civicrm_group.id";
363 }
364 else {
365 $includeGroup = "INSERT INTO Ig_{$this->_tableName} (contact_id, group_names)
366 SELECT civicrm_contact.id as contact_id, ''
367 FROM civicrm_contact";
368 }
369
370
371 //used only when exclude group is selected
372 if ($xGroups != 0) {
373 $includeGroup .= " LEFT JOIN Xg_{$this->_tableName}
374 ON civicrm_contact.id = Xg_{$this->_tableName}.contact_id";
375 }
376
377 if ($iGroups) {
378 $includeGroup .= " WHERE
379 civicrm_group_contact.status = 'Added' AND
380 civicrm_group_contact.group_id IN($iGroups)";
381 }
382 else {
383 $includeGroup .= " WHERE ( 1 ) ";
384 }
385
386 //used only when exclude group is selected
387 if ($xGroups != 0) {
388 $includeGroup .= " AND Xg_{$this->_tableName}.contact_id IS null";
389 }
390
391 CRM_Core_DAO::executeQuery($includeGroup);
392
393 //search for smart group contacts
394
395 foreach ($this->_includeGroups as $keys => $values) {
396 if (in_array($values, $smartGroup)) {
397 $ssGroup = new CRM_Contact_DAO_Group();
398 $ssGroup->id = $values;
399 if (!$ssGroup->find(TRUE)) {
400 CRM_Core_Error::fatal();
401 }
402 CRM_Contact_BAO_GroupContactCache::load($ssGroup);
403
404 $smartSql = "
405 SELECT gcc.contact_id
406 FROM civicrm_group_contact_cache gcc
407 WHERE gcc.group_id = {$ssGroup->id}
408 ";
409
410 //used only when exclude group is selected
411 if ($xGroups != 0) {
412 $smartSql .= " AND gcc.contact_id NOT IN (SELECT contact_id FROM Xg_{$this->_tableName})";
413 }
414
415 $smartGroupQuery = " INSERT IGNORE INTO Ig_{$this->_tableName}(contact_id)
416 $smartSql";
417
418 CRM_Core_DAO::executeQuery($smartGroupQuery);
419 $insertGroupNameQuery = "UPDATE IGNORE Ig_{$this->_tableName}
420 SET group_names = (SELECT title FROM civicrm_group
421 WHERE civicrm_group.id = $values)
422 WHERE Ig_{$this->_tableName}.contact_id IS NOT NULL
423 AND Ig_{$this->_tableName}.group_names IS NULL";
424 CRM_Core_DAO::executeQuery($insertGroupNameQuery);
425 }
426 }
427 }
428 //group contact search end here;
429
430 //block for Tags search
431 if ($this->_tags || $this->_allSearch) {
432 //find all tags
433 $tag = new CRM_Core_DAO_Tag();
434 $tag->is_active = 1;
435 $tag->find();
436 while ($tag->fetch()) {
437 $allTags[] = $tag->id;
438 }
439 $includedTags = implode(',', $allTags);
440
441 if (!empty($this->_includeTags)) {
442 $iTags = implode(',', $this->_includeTags);
443 }
444 else {
445 //if no group selected search for all groups
446 $iTags = NULL;
447 }
448 if (is_array($this->_excludeTags)) {
449 $xTags = implode(',', $this->_excludeTags);
450 }
451 else {
452 $xTags = 0;
453 }
454
455 $sql = "CREATE TEMPORARY TABLE Xt_{$this->_tableName} ( contact_id int primary key) ENGINE=MyISAM";
456 CRM_Core_DAO::executeQuery($sql);
457
458 //used only when exclude tag is selected
459 if ($xTags != 0) {
460 $excludeTag = "INSERT INTO Xt_{$this->_tableName} ( contact_id )
461 SELECT DISTINCT civicrm_entity_tag.entity_id
462 FROM civicrm_entity_tag, civicrm_contact
463 WHERE
464 civicrm_entity_tag.entity_table = 'civicrm_contact' AND
465 civicrm_contact.id = civicrm_entity_tag.entity_id AND
466 civicrm_entity_tag.tag_id IN( {$xTags})";
467
468 CRM_Core_DAO::executeQuery($excludeTag);
469 }
470
471 $sql = "CREATE TEMPORARY TABLE It_{$this->_tableName} ( id int PRIMARY KEY AUTO_INCREMENT,
472 contact_id int,
473 tag_names varchar(64)) ENGINE=MyISAM";
474
475 CRM_Core_DAO::executeQuery($sql);
476
477 if ($iTags) {
478 $includeTag = "INSERT INTO It_{$this->_tableName} (contact_id, tag_names)
479 SELECT civicrm_contact.id as contact_id, civicrm_tag.name as tag_name
480 FROM civicrm_contact
481 INNER JOIN civicrm_entity_tag
482 ON ( civicrm_entity_tag.entity_table = 'civicrm_contact' AND
483 civicrm_entity_tag.entity_id = civicrm_contact.id )
484 LEFT JOIN civicrm_tag
485 ON civicrm_entity_tag.tag_id = civicrm_tag.id";
486 }
487 else {
488 $includeTag = "INSERT INTO It_{$this->_tableName} (contact_id, tag_names)
489 SELECT civicrm_contact.id as contact_id, ''
490 FROM civicrm_contact";
491 }
492
493 //used only when exclude tag is selected
494 if ($xTags != 0) {
495 $includeTag .= " LEFT JOIN Xt_{$this->_tableName}
496 ON civicrm_contact.id = Xt_{$this->_tableName}.contact_id";
497 }
498 if ($iTags) {
499 $includeTag .= " WHERE civicrm_entity_tag.tag_id IN($iTags)";
500 }
501 else {
502 $includeTag .= " WHERE ( 1 ) ";
503 }
504
505 //used only when exclude tag is selected
506 if ($xTags != 0) {
507 $includeTag .= " AND Xt_{$this->_tableName}.contact_id IS null";
508 }
509
510 CRM_Core_DAO::executeQuery($includeTag);
511 }
512
513 $from = " FROM civicrm_contact contact_a";
514
515 /*
516 * CRM-10850 / CRM-10848
517 * If we use include / exclude groups as smart groups for ACL's having the below causes
518 * a cycle which messes things up. Hence commenting out for now
519 * $this->buildACLClause('contact_a');
520 */
521
522 /*
523 * check the situation and set booleans
524 */
525 $Ig = ($iGroups != 0);
526 $It = ($iTags != 0);
527 $Xg = ($xGroups != 0);
528 $Xt = ($xTags != 0);
529
530 //PICK UP FROM HERE
531 if (!$this->_groups && !$this->_tags) {
532 $this->_andOr = 1;
533 }
534
535 /*
536 * Set from statement depending on array sel
537 */
538 $whereitems = array();
539 foreach (array('Ig', 'It') as $inc) {
540 if ($this->_andOr == 1) {
541 if ($$inc) {
542 $from .= " INNER JOIN {$inc}_{$this->_tableName} temptable$inc ON (contact_a.id = temptable$inc.contact_id)";
543 }
544 }
545 else {
546 if ($$inc) {
547 $from .= " LEFT JOIN {$inc}_{$this->_tableName} temptable$inc ON (contact_a.id = temptable$inc.contact_id)";
548 }
549 }
550 if ($$inc) {
551 $whereitems[] = "temptable$inc.contact_id IS NOT NULL";
552 }
553 }
554 $this->_where = $whereitems ? "(" . implode(' OR ', $whereitems) . ')' : '(1)';
555 foreach (array('Xg', 'Xt') as $exc) {
556 if ($$exc) {
557 $from .= " LEFT JOIN {$exc}_{$this->_tableName} temptable$exc ON (contact_a.id = temptable$exc.contact_id)";
558 $this->_where .= " AND temptable$exc.contact_id IS NULL";
559 }
560 }
561
562 $from .= " LEFT JOIN civicrm_email ON ( contact_a.id = civicrm_email.contact_id AND ( civicrm_email.is_primary = 1 OR civicrm_email.is_bulkmail = 1 ) ) {$this->_aclFrom}";
563
564 if ($this->_aclWhere) {
565 $this->_where .= " AND {$this->_aclWhere} ";
566 }
567
568 // also exclude all contacts that are deleted
569 // CRM-11627
570 $this->_where .= " AND (contact_a.is_deleted != 1) ";
571
572 return $from;
573 }
574
575 /**
576 * @param bool $includeContactIDs
577 *
578 * @return string
579 */
580 function where($includeContactIDs = FALSE) {
581 if ($includeContactIDs) {
582 $contactIDs = array();
583
584 foreach ($this->_formValues as $id => $value) {
585 if ($value &&
586 substr($id, 0, CRM_Core_Form::CB_PREFIX_LEN) == CRM_Core_Form::CB_PREFIX
587 ) {
588 $contactIDs[] = substr($id, CRM_Core_Form::CB_PREFIX_LEN);
589 }
590 }
591
592 if (!empty($contactIDs)) {
593 $contactIDs = implode(', ', $contactIDs);
594 $clauses[] = "contact_a.id IN ( $contactIDs )";
595 }
596 $where = "{$this->_where} AND " . implode(' AND ', $clauses);
597 }
598 else {
599 $where = $this->_where;
600 }
601
602 return $where;
603 }
604
605 /*
606 * Functions below generally don't need to be modified
607 */
608 /**
609 * @return mixed
610 */
611 function count() {
612 $sql = $this->all();
613
614 $dao = CRM_Core_DAO::executeQuery($sql);
615 return $dao->N;
616 }
617
618 /**
619 * @param int $offset
620 * @param int $rowcount
621 * @param null $sort
622 * @param bool $returnSQL
623 *
624 * @return string
625 */
626 function contactIDs($offset = 0, $rowcount = 0, $sort = NULL, $returnSQL = FALSE) {
627 return $this->all($offset, $rowcount, $sort, FALSE, TRUE);
628 }
629
630 /**
631 * @return array
632 */
633 function &columns() {
634 return $this->_columns;
635 }
636
637 /**
638 * @return null
639 */
640 function summary() {
641 return NULL;
642 }
643
644 /**
645 * @return string
646 */
647 function templateFile() {
648 return 'CRM/Contact/Form/Search/Custom.tpl';
649 }
650
651 /**
652 * @param $title
653 */
654 function setTitle($title) {
655 if ($title) {
656 CRM_Utils_System::setTitle($title);
657 }
658 else {
659 CRM_Utils_System::setTitle(ts('Search'));
660 }
661 }
662
663 /**
664 * @param string $tableAlias
665 */
666 function buildACLClause($tableAlias = 'contact') {
667 list($this->_aclFrom, $this->_aclWhere) = CRM_Contact_BAO_Contact_Permission::cacheClause($tableAlias);
668 }
669 }
670