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