Merge pull request #4781 from civicrm/CRM-15732
[civicrm-core.git] / CRM / Contact / Selector.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
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 */
35
36/**
37 * This class is used to retrieve and display a range of
38 * contacts that match the given criteria (specifically for
39 * results of advanced search options.
40 *
41 */
42class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Selector_API {
43
44 /**
45 * This defines two actions- View and Edit.
46 *
47 * @var array
48 * @static
49 */
50 static $_links = NULL;
51
52 /**
100fef9d 53 * We use desc to remind us what that column is, name is used in the tpl
6a488035
TO
54 *
55 * @var array
56 * @static
57 */
58 static $_columnHeaders;
59
60 /**
61 * Properties of contact we're interested in displaying
62 * @var array
63 * @static
64 */
65 static $_properties = array(
66 'contact_id', 'contact_type', 'contact_sub_type',
67 'sort_name', 'street_address',
68 'city', 'state_province', 'postal_code', 'country',
69 'geo_code_1', 'geo_code_2', 'is_deceased',
70 'email', 'on_hold', 'phone', 'status',
71 'do_not_email', 'do_not_phone', 'do_not_mail',
72 );
73
74 /**
100fef9d 75 * FormValues is the array returned by exportValues called on
6a488035
TO
76 * the HTML_QuickForm_Controller for that page.
77 *
78 * @var array
79 * @access protected
80 */
81 public $_formValues;
82
83 /**
84 * The contextMenu
85 *
86 * @var array
87 * @access protected
88 */
89 protected $_contextMenu;
90
91 /**
100fef9d 92 * Params is the array in a value used by the search query creator
6a488035
TO
93 *
94 * @var array
95 * @access protected
96 */
97 public $_params;
98
99 /**
100 * The return properties used for search
101 *
102 * @var array
103 * @access protected
104 */
105 protected $_returnProperties;
106
107 /**
100fef9d 108 * Represent the type of selector
6a488035
TO
109 *
110 * @var int
111 * @access protected
112 */
113 protected $_action;
114
115 protected $_searchContext;
116
117 protected $_query;
118
119 /**
100fef9d 120 * Group id
6a488035
TO
121 *
122 * @var int
123 */
124 protected $_ufGroupID;
125
126 /**
100fef9d 127 * The public visible fields to be shown to the user
6a488035
TO
128 *
129 * @var array
130 * @access protected
131 */
132 protected $_fields;
133
134 /**
135 * Class constructor
136 *
77b97be7 137 * @param $customSearchClass
6a488035 138 * @param array $formValues array of form values imported
77b97be7
EM
139 * @param array $params array of parameters for query
140 * @param null $returnProperties
141 * @param \const|int $action - action of search basic or advanced.
142 *
143 * @param bool $includeContactIds
144 * @param bool $searchDescendentGroups
145 * @param string $searchContext
146 * @param null $contextMenu
6a488035
TO
147 *
148 * @return CRM_Contact_Selector
149 * @access public
150 */
151 function __construct(
152 $customSearchClass,
153 $formValues = NULL,
154 $params = NULL,
155 $returnProperties = NULL,
156 $action = CRM_Core_Action::NONE,
157 $includeContactIds = FALSE,
158 $searchDescendentGroups = TRUE,
159 $searchContext = 'search',
160 $contextMenu = NULL
161 ) {
162 //don't build query constructor, if form is not submitted
163 $force = CRM_Utils_Request::retrieve('force', 'Boolean', CRM_Core_DAO::$_nullObject);
164 if (empty($formValues) && !$force) {
165 return;
166 }
167
168 // submitted form values
169 $this->_formValues = &$formValues;
170 $this->_params = &$params;
171 $this->_returnProperties = &$returnProperties;
172 $this->_contextMenu = &$contextMenu;
173 $this->_context = $searchContext;
174
175 // type of selector
176 $this->_action = $action;
177
178 $this->_searchContext = $searchContext;
179
180 $this->_ufGroupID = CRM_Utils_Array::value('uf_group_id', $this->_formValues);
181
182 if ($this->_ufGroupID) {
183 $this->_fields = CRM_Core_BAO_UFGroup::getListingFields(CRM_Core_Action::VIEW,
184 CRM_Core_BAO_UFGroup::PUBLIC_VISIBILITY |
185 CRM_Core_BAO_UFGroup::LISTINGS_VISIBILITY,
186 FALSE, $this->_ufGroupID
187 );
188 self::$_columnHeaders = NULL;
189
190 $this->_customFields = CRM_Core_BAO_CustomField::getFieldsForImport('Individual');
191
192 $this->_returnProperties = CRM_Contact_BAO_Contact::makeHierReturnProperties($this->_fields);
193 $this->_returnProperties['contact_type'] = 1;
194 $this->_returnProperties['contact_sub_type'] = 1;
195 $this->_returnProperties['sort_name'] = 1;
196 }
197
198 $displayRelationshipType = CRM_Utils_Array::value('display_relationship_type', $this->_formValues);
199 $operator = CRM_Utils_Array::value('operator', $this->_formValues, 'AND');
200
201 // rectify params to what proximity search expects if there is a value for prox_distance
202 // CRM-7021
203 if (!empty($this->_params)) {
204 CRM_Contact_BAO_ProximityQuery::fixInputParams($this->_params);
205 }
206
207 $this->_query = new CRM_Contact_BAO_Query(
208 $this->_params,
209 $this->_returnProperties,
210 NULL,
211 $includeContactIds,
212 FALSE,
213 CRM_Contact_BAO_Query::MODE_CONTACTS,
214 FALSE,
215 $searchDescendentGroups,
216 FALSE,
217 $displayRelationshipType,
218 $operator
219 );
220
221 $this->_options = &$this->_query->_options;
222 }
6a488035
TO
223
224 /**
225 * This method returns the links that are given for each search row.
226 * currently the links added for each row are
227 *
228 * - View
229 * - Edit
230 *
231 * @return array
232 * @access public
233 *
234 */
235 static function &links() {
236 list($context, $contextMenu, $key) = func_get_args();
237 $extraParams = ($key) ? "&key={$key}" : NULL;
238 $searchContext = ($context) ? "&context=$context" : NULL;
239
240 if (!(self::$_links)) {
241 self::$_links = array(
242 CRM_Core_Action::VIEW => array(
243 'name' => ts('View'),
244 'url' => 'civicrm/contact/view',
8e4ec1f5 245 'class' => 'no-popup',
6a488035
TO
246 'qs' => "reset=1&cid=%%id%%{$searchContext}{$extraParams}",
247 'title' => ts('View Contact Details'),
248 'ref' => 'view-contact',
249 ),
250 CRM_Core_Action::UPDATE => array(
251 'name' => ts('Edit'),
252 'url' => 'civicrm/contact/add',
8e4ec1f5 253 'class' => 'no-popup',
6a488035
TO
254 'qs' => "reset=1&action=update&cid=%%id%%{$searchContext}{$extraParams}",
255 'title' => ts('Edit Contact Details'),
256 'ref' => 'edit-contact',
257 ),
258 );
259
260 $config = CRM_Core_Config::singleton();
261 if ($config->mapAPIKey && $config->mapProvider) {
262 self::$_links[CRM_Core_Action::MAP] = array(
263 'name' => ts('Map'),
264 'url' => 'civicrm/contact/map',
265 'qs' => "reset=1&cid=%%id%%{$searchContext}{$extraParams}",
266 'title' => ts('Map Contact'),
267 );
268 }
269
270 // Adding Context Menu Links in more action
271 if ($contextMenu) {
272 $counter = 7000;
273 foreach ($contextMenu as $key => $value) {
274 $contextVal = '&context=' . $value['key'];
275 if ($value['key'] == 'delete') {
276 $contextVal = $searchContext;
277 }
6a488035
TO
278 $url = "civicrm/contact/view/{$value['key']}";
279 $qs = "reset=1&action=add&cid=%%id%%{$contextVal}{$extraParams}";
280 if ($value['key'] == 'activity') {
281 $qs = "action=browse&selectedChild=activity&reset=1&cid=%%id%%{$extraParams}";
282 }
283 elseif ($value['key'] == 'email') {
284 $url = "civicrm/contact/view/activity";
285 $qs = "atype=3&action=add&reset=1&cid=%%id%%{$extraParams}";
286 }
287
288 self::$_links[$counter++] = array(
289 'name' => $value['title'],
290 'url' => $url,
291 'qs' => $qs,
292 'title' => $value['title'],
293 'ref' => $value['ref'],
8e4ec1f5 294 'class' => CRM_Utils_Array::value('class', $value),
6a488035
TO
295 );
296 }
297 }
298 }
299 return self::$_links;
300 }
6a488035
TO
301
302 /**
100fef9d 303 * Getter for array of the parameters required for creating pager.
6a488035 304 *
77b97be7 305 * @param $action
c490a46a 306 * @param array $params
77b97be7 307 *
6a488035
TO
308 * @access public
309 */
310 function getPagerParams($action, &$params) {
311 $params['status'] = ts('Contact %%StatusMessage%%');
312 $params['csvString'] = NULL;
313 $params['rowCount'] = CRM_Utils_Pager::ROWCOUNT;
314
315 $params['buttonTop'] = 'PagerTopButton';
316 $params['buttonBottom'] = 'PagerBottomButton';
317 }
6a488035 318
86538308
EM
319 /**
320 * @param null $action
321 * @param null $output
322 *
323 * @return array
324 */
5defa2fd 325 function &getColHeads($action = NULL, $output = NULL) {
6a488035 326 $colHeads = self::_getColumnHeaders();
6a488035
TO
327 $colHeads[] = array('desc' => ts('Actions'), 'name' => ts('Action'));
328 return $colHeads;
329 }
330
331 /**
100fef9d 332 * Returns the column headers as an array of tuples:
6a488035
TO
333 * (name, sortName (key to the sort array))
334 *
335 * @param string $action the action being performed
336 * @param enum $output what should the result set include (web/email/csv)
337 *
338 * @return array the column headers that need to be displayed
339 * @access public
340 */
341 function &getColumnHeaders($action = NULL, $output = NULL) {
342 $headers = NULL;
b3eeb853 343
344 // unset return property elements that we don't care
345 if (!empty($this->_returnProperties)) {
346 $doNotCareElements = array(
347 'contact_type',
348 'contact_sub_type',
349 'sort_name',
350 );
351 foreach ( $doNotCareElements as $value) {
352 unset($this->_returnProperties[$value]);
353 }
354 }
355
6a488035 356 if ($output == CRM_Core_Selector_Controller::EXPORT) {
7b99ead3 357 $csvHeaders = array(ts('Contact ID'), ts('Contact Type'));
6a488035
TO
358 foreach ($this->getColHeads($action, $output) as $column) {
359 if (array_key_exists('name', $column)) {
360 $csvHeaders[] = $column['name'];
361 }
362 }
363 $headers = $csvHeaders;
364 }
365 elseif ($output == CRM_Core_Selector_Controller::SCREEN) {
366 $csvHeaders = array(ts('Name'));
367 foreach ($this->getColHeads($action, $output) as $key => $column) {
368 if (array_key_exists('name', $column) &&
369 $column['name'] &&
370 $column['name'] != ts('Name')
371 ) {
372 $csvHeaders[$key] = $column['name'];
373 }
374 }
375 $headers = $csvHeaders;
376 }
377 elseif ($this->_ufGroupID) {
378 // we dont use the cached value of column headers
379 // since it potentially changed because of the profile selected
380 static $skipFields = array('group', 'tag');
381 $direction = CRM_Utils_Sort::ASCENDING;
382 $empty = TRUE;
383 if (!self::$_columnHeaders) {
384 self::$_columnHeaders = array(array('name' => ''),
385 array(
386 'name' => ts('Name'),
387 'sort' => 'sort_name',
388 'direction' => CRM_Utils_Sort::ASCENDING,
389 ),
390 );
391
b2b0530a 392 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
6a488035
TO
393
394 foreach ($this->_fields as $name => $field) {
a7488080 395 if (!empty($field['in_selector']) &&
6a488035
TO
396 !in_array($name, $skipFields)
397 ) {
398 if (strpos($name, '-') !== FALSE) {
399 list($fieldName, $lType, $type) = CRM_Utils_System::explode('-', $name, 3);
400
401 if ($lType == 'Primary') {
402 $locationTypeName = 1;
403 }
404 else {
405 $locationTypeName = $locationTypes[$lType];
406 }
407
408 if (in_array($fieldName, array(
409 'phone', 'im', 'email'))) {
410 if ($type) {
411 $name = "`$locationTypeName-$fieldName-$type`";
412 }
413 else {
414 $name = "`$locationTypeName-$fieldName`";
415 }
416 }
417 else {
418 $name = "`$locationTypeName-$fieldName`";
419 }
420 }
421 //to handle sort key for Internal contactId.CRM-2289
422 if ($name == 'id') {
423 $name = 'contact_id';
424 }
425
426 self::$_columnHeaders[] = array(
427 'name' => $field['title'],
428 'sort' => $name,
429 'direction' => $direction,
430 );
431 $direction = CRM_Utils_Sort::DONTCARE;
432 $empty = FALSE;
433 }
434 }
435
436 // if we dont have any valid columns, dont add the implicit ones
437 // this allows the template to check on emptiness of column headers
438 if ($empty) {
439 self::$_columnHeaders = array();
440 }
441 else {
442 self::$_columnHeaders[] = array('desc' => ts('Actions'), 'name' => ts('Action'));
443 }
444 }
445 $headers = self::$_columnHeaders;
446 }
447 elseif (!empty($this->_returnProperties)) {
6a488035
TO
448 self::$_columnHeaders = array(array('name' => ''),
449 array(
450 'name' => ts('Name'),
451 'sort' => 'sort_name',
452 'direction' => CRM_Utils_Sort::ASCENDING,
453 ),
454 );
455 $properties = self::makeProperties($this->_returnProperties);
456
457 foreach ($properties as $prop) {
6a488035
TO
458 if (strpos($prop, '-')) {
459 list($loc, $fld, $phoneType) = CRM_Utils_System::explode('-', $prop, 3);
460 $title = $this->_query->_fields[$fld]['title'];
461 if (trim($phoneType) && !is_numeric($phoneType) && strtolower($phoneType) != $fld) {
462 $title .= "-{$phoneType}";
463 }
464 $title .= " ($loc)";
465 }
466 elseif (isset($this->_query->_fields[$prop]) && isset($this->_query->_fields[$prop]['title'])) {
467 $title = $this->_query->_fields[$prop]['title'];
b3eeb853 468 }
469 else {
6a488035
TO
470 $title = '';
471 }
472
473 self::$_columnHeaders[] = array('name' => $title, 'sort' => $prop);
474 }
475 self::$_columnHeaders[] = array('name' => ts('Actions'));
476 $headers = self::$_columnHeaders;
477 }
478 else {
479 $headers = $this->getColHeads($action, $output);
480 }
481
482 return $headers;
483 }
484
485 /**
486 * Returns total number of rows for the query.
487 *
488 * @param
489 *
490 * @return int Total number of rows
491 * @access public
492 */
493 function getTotalCount($action) {
f0c8c107
CW
494 // Use count from cache during paging/sorting
495 if (!empty($_GET['crmPID']) || !empty($_GET['crmSID'])) {
496 $count = CRM_Core_BAO_Cache::getItem('Search Results Count', $this->_key);
497 }
498 if (empty($count)) {
499 $count = $this->_query->searchQuery(0, 0, NULL, TRUE);
500 CRM_Core_BAO_Cache::setItem($count, 'Search Results Count', $this->_key);
501 }
502 return $count;
6a488035
TO
503 }
504
505 /**
100fef9d 506 * Returns all the rows in the given offset and rowCount
6a488035
TO
507 *
508 * @param enum $action the action being performed
509 * @param int $offset the row number to start from
510 * @param int $rowCount the number of rows to return
511 * @param string $sort the sql string that describes the sort order
512 * @param enum $output what should the result set include (web/email/csv)
513 *
514 * @return int the total number of rows for this action
515 */
516 function &getRows($action, $offset, $rowCount, $sort, $output = NULL) {
517 $config = CRM_Core_Config::singleton();
518
519 if (($output == CRM_Core_Selector_Controller::EXPORT ||
520 $output == CRM_Core_Selector_Controller::SCREEN
521 ) &&
522 $this->_formValues['radio_ts'] == 'ts_sel'
523 ) {
524 $includeContactIds = TRUE;
525 }
526 else {
527 $includeContactIds = FALSE;
528 }
529
530 // note the formvalues were given by CRM_Contact_Form_Search to us
531 // and contain the search criteria (parameters)
532 // note that the default action is basic
4243847f
CW
533 if ($rowCount) {
534 $cacheKey = $this->buildPrevNextCache($sort);
535 $result = $this->_query->getCachedContacts($cacheKey, $offset, $rowCount, $includeContactIds);
536 }
537 else {
538 $result = $this->_query->searchQuery($offset, $rowCount, $sort, FALSE, $includeContactIds);
539 }
6a488035
TO
540
541 // process the result of the query
542 $rows = array();
543 $permissions = array(CRM_Core_Permission::getPermission());
544 if (CRM_Core_Permission::check('delete contacts')) {
545 $permissions[] = CRM_Core_Permission::DELETE;
546 }
547 $mask = CRM_Core_Action::mask($permissions);
548
549 // mask value to hide map link if there are not lat/long
550 $mapMask = $mask & 4095;
551
552 if ($this->_searchContext == 'smog') {
553 $gc = CRM_Core_SelectValues::groupContactStatus();
554 }
555
556 if ($this->_ufGroupID) {
b2b0530a 557 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
6a488035
TO
558
559 $names = array();
560 static $skipFields = array('group', 'tag');
561 foreach ($this->_fields as $key => $field) {
a7488080 562 if (!empty($field['in_selector']) &&
6a488035
TO
563 !in_array($key, $skipFields)
564 ) {
565 if (strpos($key, '-') !== FALSE) {
566 list($fieldName, $id, $type) = CRM_Utils_System::explode('-', $key, 3);
567
568 if ($id == 'Primary') {
569 $locationTypeName = 1;
570 }
571 else {
572 $locationTypeName = CRM_Utils_Array::value($id, $locationTypes);
573 if (!$locationTypeName) {
574 continue;
575 }
576 }
577
578 $locationTypeName = str_replace(' ', '_', $locationTypeName);
579 if (in_array($fieldName, array(
580 'phone', 'im', 'email'))) {
581 if ($type) {
582 $names[] = "{$locationTypeName}-{$fieldName}-{$type}";
583 }
584 else {
585 $names[] = "{$locationTypeName}-{$fieldName}";
586 }
587 }
588 else {
589 $names[] = "{$locationTypeName}-{$fieldName}";
590 }
591 }
592 else {
593 $names[] = $field['name'];
594 }
595 }
596 }
597
598 $names[] = "status";
599 }
600 elseif (!empty($this->_returnProperties)) {
601 $names = self::makeProperties($this->_returnProperties);
602 }
603 else {
604 $names = self::$_properties;
605 }
606
607 $multipleSelectFields = array('preferred_communication_method' => 1);
608
609 $links = self::links($this->_context, $this->_contextMenu, $this->_key);
610
611 //check explicitly added contact to a Smart Group.
612 $groupID = CRM_Utils_Array::key('1', $this->_formValues['group']);
613
0c145cc0 614 $pseudoconstants = array();
6a488035 615 // for CRM-3157 purposes
6a488035 616 if (in_array('world_region', $names)) {
0c145cc0
DL
617 $pseudoconstants['world_region'] = array(
618 'dbName' => 'world_region_id',
619 'values' => CRM_Core_PseudoConstant::worldRegion()
620 );
6a488035
TO
621 }
622
623 $seenIDs = array();
624 while ($result->fetch()) {
625 $row = array();
d9ab802d 626 $this->_query->convertToPseudoNames($result);
6a488035
TO
627
628 // the columns we are interested in
629 foreach ($names as $property) {
630 if ($property == 'status') {
631 continue;
632 }
633 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($property)) {
0c145cc0
DL
634 $row[$property] = CRM_Core_BAO_CustomField::getDisplayValue(
635 $result->$property,
6a488035
TO
636 $cfID,
637 $this->_options,
638 $result->contact_id
639 );
640 }
0c145cc0
DL
641 elseif (
642 $multipleSelectFields &&
6a488035
TO
643 array_key_exists($property, $multipleSelectFields)
644 ) {
6a488035
TO
645 $key = $property;
646 $paramsNew = array($key => $result->$property);
0c145cc0 647 $name = array($key => array('newName' => $key, 'groupName' => $key));
6a488035 648
6a488035
TO
649 CRM_Core_OptionGroup::lookupValues($paramsNew, $name, FALSE);
650 $row[$key] = $paramsNew[$key];
651 }
652 elseif (strpos($property, '-im')) {
653 $row[$property] = $result->$property;
654 if (!empty($result->$property)) {
e7e657f0 655 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
6a488035
TO
656 $providerId = $property . "-provider_id";
657 $providerName = $imProviders[$result->$providerId];
658 $row[$property] = $result->$property . " ({$providerName})";
659 }
660 }
661 elseif (in_array($property, array(
662 'addressee', 'email_greeting', 'postal_greeting'))) {
663 $greeting = $property . '_display';
664 $row[$property] = $result->$greeting;
665 }
0c145cc0
DL
666 elseif (isset($pseudoconstants[$property])) {
667 $row[$property] = CRM_Utils_Array::value(
668 $result->{$pseudoconstants[$property]['dbName']},
669 $pseudoconstants[$property]['values']
670 );
6a488035
TO
671 }
672 elseif (strpos($property, '-url') !== FALSE) {
673 $websiteUrl = '';
674 $websiteKey = 'website-1';
675 $propertyArray = explode('-', $property);
676 $websiteFld = $websiteKey . '-' . array_pop($propertyArray);
677 if (!empty($result->$websiteFld)) {
cbf48754 678 $websiteTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Website', 'website_type_id');
6a488035
TO
679 $websiteType = $websiteTypes[$result->{"$websiteKey-website_type_id"}];
680 $websiteValue = $result->$websiteFld;
681 $websiteUrl = "<a href=\"{$websiteValue}\">{$websiteValue} ({$websiteType})</a>";
682 }
683 $row[$property] = $websiteUrl;
684 }
685 else {
686 $row[$property] = isset($result->$property) ? $result->$property : NULL;
687 }
688 }
689
690 if (!empty($result->postal_code_suffix)) {
691 $row['postal_code'] .= "-" . $result->postal_code_suffix;
692 }
693
694 if ($output != CRM_Core_Selector_Controller::EXPORT &&
695 $this->_searchContext == 'smog'
696 ) {
697 if (empty($result->status) &&
698 $groupID
699 ) {
700 $contactID = $result->contact_id;
701 if ($contactID) {
702 $gcParams = array(
703 'contact_id' => $contactID,
704 'group_id' => $groupID,
705 );
706
707 $gcDefaults = array();
708 CRM_Core_DAO::commonRetrieve('CRM_Contact_DAO_GroupContact', $gcParams, $gcDefaults);
709
710 if (empty($gcDefaults)) {
711 $row['status'] = ts('Smart');
712 }
713 else {
714 $row['status'] = $gc[$gcDefaults['status']];
715 }
716 }
717 else {
718 $row['status'] = NULL;
719 }
720 }
721 else {
722 $row['status'] = $gc[$result->status];
723 }
724 }
725
726 if ($output != CRM_Core_Selector_Controller::EXPORT) {
727 $row['checkbox'] = CRM_Core_Form::CB_PREFIX . $result->contact_id;
728
a7488080 729 if (!empty($this->_formValues['deleted_contacts']) && CRM_Core_Permission::check('access deleted contacts')
6a488035
TO
730 ) {
731 $links = array(
732 array(
733 'name' => ts('View'),
734 'url' => 'civicrm/contact/view',
735 'qs' => 'reset=1&cid=%%id%%',
8e4ec1f5 736 'class' => 'no-popup',
6a488035
TO
737 'title' => ts('View Contact Details'),
738 ),
739 array(
740 'name' => ts('Restore'),
741 'url' => 'civicrm/contact/view/delete',
742 'qs' => 'reset=1&cid=%%id%%&restore=1',
743 'title' => ts('Restore Contact'),
744 ),
745 );
746 if (CRM_Core_Permission::check('delete contacts')) {
747 $links[] = array(
748 'name' => ts('Delete Permanently'),
749 'url' => 'civicrm/contact/view/delete',
750 'qs' => 'reset=1&cid=%%id%%&skip_undelete=1',
751 'title' => ts('Permanently Delete Contact'),
752 );
753 }
87dab4a4
AH
754 $row['action'] = CRM_Core_Action::formLink(
755 $links,
756 NULL,
757 array('id' => $result->contact_id),
758 ts('more'),
759 FALSE,
760 'contact.selector.row',
761 'Contact',
762 $result->contact_id
763 );
6a488035
TO
764 }
765 elseif ((is_numeric(CRM_Utils_Array::value('geo_code_1', $row))) ||
8cc574cf 766 ($config->mapGeoCoding && !empty($row['city']) &&
6a488035
TO
767 CRM_Utils_Array::value('state_province', $row)
768 )
769 ) {
87dab4a4
AH
770 $row['action'] = CRM_Core_Action::formLink(
771 $links,
772 $mask,
773 array('id' => $result->contact_id),
774 ts('more'),
775 FALSE,
776 'contact.selector.row',
777 'Contact',
778 $result->contact_id
779 );
6a488035
TO
780 }
781 else {
87dab4a4
AH
782 $row['action'] = CRM_Core_Action::formLink(
783 $links,
784 $mapMask,
785 array('id' => $result->contact_id),
786 ts('more'),
787 FALSE,
788 'contact.selector.row',
789 'Contact',
790 $result->contact_id
791 );
6a488035
TO
792 }
793
794 // allow components to add more actions
795 CRM_Core_Component::searchAction($row, $result->contact_id);
796
797 $row['contact_type'] = CRM_Contact_BAO_Contact_Utils::getImage($result->contact_sub_type ?
798 $result->contact_sub_type : $result->contact_type,
799 FALSE,
800 $result->contact_id
801 );
802
2bbddafc 803 $row['contact_type_orig'] = $result->contact_sub_type ? $result->contact_sub_type : $result->contact_type;
6a488035
TO
804 $row['contact_sub_type'] = $result->contact_sub_type ?
805 CRM_Contact_BAO_ContactType::contactTypePairs(FALSE, $result->contact_sub_type, ', ') : $result->contact_sub_type;
806 $row['contact_id'] = $result->contact_id;
807 $row['sort_name'] = $result->sort_name;
808 if (array_key_exists('id', $row)) {
809 $row['id'] = $result->contact_id;
810 }
811 }
812
813 // Dedupe contacts
814 if (in_array($row['contact_id'], $seenIDs) === FALSE) {
815 $seenIDs[] = $row['contact_id'];
816 $rows[] = $row;
817 }
818 }
819
6a488035
TO
820 return $rows;
821 }
822
86538308 823 /**
c490a46a 824 * @param CRM_Utils_Sort $sort
86538308
EM
825 *
826 * @return string
827 */
6a488035 828 function buildPrevNextCache($sort) {
4243847f
CW
829 $cacheKey = 'civicrm search ' . $this->_key;
830
ddf14bea 831 // We should clear the cache in following conditions:
832 // 1. when starting from scratch, i.e new search
833 // 2. if records are sorted
834
835 // get current page requested
4243847f 836 $pageNum = CRM_Utils_Request::retrieve('crmPID', 'Integer', CRM_Core_DAO::$_nullObject);
ddf14bea 837
838 // get the current sort order
839 $currentSortID = CRM_Utils_Request::retrieve('crmSID', 'String', CRM_Core_DAO::$_nullObject);
840
841 $session = CRM_Core_Session::singleton();
842
843 // get previous sort id
844 $previousSortID = $session->get('previousSortID');
845
846 // check for current != previous to ensure cache is not reset if paging is done without changing
847 // sort criteria
848 if (!$pageNum || (!empty($currentSortID) && $currentSortID != $previousSortID) ) {
4243847f 849 CRM_Core_BAO_PrevNextCache::deleteItem(NULL, $cacheKey, 'civicrm_contact');
ddf14bea 850 // this means it's fresh search, so set pageNum=1
851 if (!$pageNum) {
852 $pageNum = 1;
853 }
854 }
855
856 // set the current sort as previous sort
857 if (!empty($currentSortID)) {
858 $session->set('previousSortID', $currentSortID);
4243847f 859 }
6a488035 860
64951b63
CW
861 $pageSize = CRM_Utils_Request::retrieve('crmRowCount', 'Integer', CRM_Core_DAO::$_nullObject, FALSE, 50);
862 $firstRecord = ($pageNum - 1) * $pageSize;
6a488035
TO
863
864 //for alphabetic pagination selection save
865 $sortByCharacter = CRM_Utils_Request::retrieve('sortByCharacter', 'String', CRM_Core_DAO::$_nullObject);
866
867 //for text field pagination selection save
4243847f 868 $countRow = CRM_Core_BAO_PrevNextCache::getCount($cacheKey, NULL, "entity_table = 'civicrm_contact'");
6a488035 869
64951b63 870 // $sortByCharacter triggers a refresh in the prevNext cache
4243847f
CW
871 if ($sortByCharacter && $sortByCharacter != 'all') {
872 $cacheKey .= "_alphabet";
873 $this->fillupPrevNextCache($sort, $cacheKey);
6a488035 874 }
64951b63 875 elseif ($firstRecord >= $countRow) {
ce613d7b 876 $this->fillupPrevNextCache($sort, $cacheKey, $countRow, 500);
64951b63 877 }
4243847f 878 return $cacheKey;
6a488035
TO
879 }
880
86538308
EM
881 /**
882 * @param $rows
883 */
6a488035
TO
884 function addActions(&$rows) {
885 $config = CRM_Core_Config::singleton();
886
887 $permissions = array(CRM_Core_Permission::getPermission());
888 if (CRM_Core_Permission::check('delete contacts')) {
889 $permissions[] = CRM_Core_Permission::DELETE;
890 }
891 $mask = CRM_Core_Action::mask($permissions);
892 // mask value to hide map link if there are not lat/long
893 $mapMask = $mask & 4095;
894
895 // mask value to hide map link if there are not lat/long
896 $mapMask = $mask & 4095;
897
898 $links = self::links($this->_context, $this->_contextMenu, $this->_key);
899
900
901 foreach ($rows as $id => & $row) {
a7488080 902 if (!empty($this->_formValues['deleted_contacts']) && CRM_Core_Permission::check('access deleted contacts')
6a488035
TO
903 ) {
904 $links = array(
905 array(
906 'name' => ts('View'),
907 'url' => 'civicrm/contact/view',
908 'qs' => 'reset=1&cid=%%id%%',
f6364403 909 'class' => 'no-popup',
6a488035
TO
910 'title' => ts('View Contact Details'),
911 ),
912 array(
913 'name' => ts('Restore'),
914 'url' => 'civicrm/contact/view/delete',
915 'qs' => 'reset=1&cid=%%id%%&restore=1',
916 'title' => ts('Restore Contact'),
917 ),
918 );
919 if (CRM_Core_Permission::check('delete contacts')) {
920 $links[] = array(
921 'name' => ts('Delete Permanently'),
922 'url' => 'civicrm/contact/view/delete',
923 'qs' => 'reset=1&cid=%%id%%&skip_undelete=1',
924 'title' => ts('Permanently Delete Contact'),
925 );
926 }
87dab4a4
AH
927 $row['action'] = CRM_Core_Action::formLink(
928 $links,
929 null,
930 array('id' => $row['contact_id']),
931 ts('more'),
932 FALSE,
933 'contact.selector.actions',
934 'Contact',
935 $row['contact_id']
936 );
6a488035
TO
937 }
938 elseif ((is_numeric(CRM_Utils_Array::value('geo_code_1', $row))) ||
8cc574cf 939 ($config->mapGeoCoding && !empty($row['city']) &&
6a488035
TO
940 CRM_Utils_Array::value('state_province', $row)
941 )
942 ) {
87dab4a4
AH
943 $row['action'] = CRM_Core_Action::formLink(
944 $links,
945 $mask,
946 array('id' => $row['contact_id']),
947 ts('more'),
948 FALSE,
949 'contact.selector.actions',
950 'Contact',
951 $row['contact_id']
952 );
6a488035
TO
953 }
954 else {
87dab4a4
AH
955 $row['action'] = CRM_Core_Action::formLink(
956 $links,
957 $mapMask,
958 array('id' => $row['contact_id']),
959 ts('more'),
960 FALSE,
961 'contact.selector.actions',
962 'Contact',
963 $row['contact_id']
964 );
6a488035
TO
965 }
966
967 // allow components to add more actions
968 CRM_Core_Component::searchAction($row, $row['contact_id']);
969
2bbddafc
CW
970 if (!empty($row['contact_type_orig'])) {
971 $row['contact_type'] = CRM_Contact_BAO_Contact_Utils::getImage($row['contact_type_orig'],
6a488035
TO
972 FALSE, $row['contact_id']);
973 }
974 }
975 }
976
86538308
EM
977 /**
978 * @param $rows
979 */
6a488035
TO
980 function removeActions(&$rows) {
981 foreach ($rows as $rid => & $rValue) {
982 unset($rValue['contact_type']);
983 unset($rValue['action']);
984 }
985 }
986
64951b63 987 /**
c490a46a 988 * @param CRM_Utils_Sort $sort
64951b63
CW
989 * @param string $cacheKey
990 * @param int $start
991 * @param int $end
992 */
4243847f 993 function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = 500) {
778a10cb 994 $coreSearch = TRUE;
64951b63 995 // For custom searches, use the contactIDs method
6a488035 996 if (is_a($this, 'CRM_Contact_Selector_Custom')) {
64951b63 997 $sql = $this->_search->contactIDs($start, $end, $sort, TRUE);
6a488035 998 $replaceSQL = "SELECT contact_a.id as contact_id";
778a10cb 999 $coreSearch = FALSE;
6a488035 1000 }
64951b63 1001 // For core searches use the searchQuery method
6a488035 1002 else {
778a10cb 1003 $sql = $this->_query->searchQuery($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
1004 FALSE, TRUE, TRUE);
6a488035
TO
1005 $replaceSQL = "SELECT contact_a.id as id";
1006 }
1007
1008 // CRM-9096
1009 // due to limitations in our search query writer, the above query does not work
1010 // in cases where the query is being sorted on a non-contact table
1011 // this results in a fatal error :(
1012 // see below for the gross hack of trapping the error and not filling
1013 // the prev next cache in this situation
1014 // the other alternative of running the FULL query will just be incredibly inefficient
1015 // and slow things down way too much on large data sets / complex queries
1016
1017 $insertSQL = "
1018INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data )
ce613d7b 1019SELECT DISTINCT 'civicrm_contact', contact_a.id, contact_a.id, '$cacheKey', contact_a.display_name
6a488035
TO
1020";
1021
1022 $sql = str_replace($replaceSQL, $insertSQL, $sql);
1023
6a4257d4 1024 $errorScope = CRM_Core_TemporaryErrorScope::ignoreException();
6a488035 1025 $result = CRM_Core_DAO::executeQuery($sql);
6a4257d4 1026 unset($errorScope);
6a488035
TO
1027
1028 if (is_a($result, 'DB_Error')) {
778a10cb 1029 // check if we get error during core search
1030 if ($coreSearch) {
1031 // in the case of error, try rebuilding cache using full sql which is used for search selector display
1032 // this fixes the bugs reported in CRM-13996 & CRM-14438
1033 $this->rebuildPreNextCache($start, $end, $sort, $cacheKey);
1034 }
1035 else {
1036 // return if above query fails
1037 return;
1038 }
6a488035
TO
1039 }
1040
1041 // also record an entry in the cache key table, so we can delete it periodically
1042 CRM_Core_BAO_Cache::setItem($cacheKey, 'CiviCRM Search PrevNextCache', $cacheKey);
1043 }
1044
778a10cb 1045 /**
1046 * This function is called to rebuild prev next cache using full sql in case of core search ( excluding custom search)
1047 *
1048 * @param int $start start for limit clause
1049 * @param int $end end for limit clause
c490a46a 1050 * @param CRM_Utils_Sort $sort
778a10cb 1051 * @param string $cacheKey cache key
1052 *
1053 * @return void
1054 */
1055 function rebuildPreNextCache($start, $end, $sort, $cacheKey) {
1056 // generate full SQL
1057 $sql = $this->_query->searchQuery($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
1058 FALSE, FALSE, TRUE);
1059
1060 $dao = CRM_Core_DAO::executeQuery($sql);
1061
1062 // build insert query, note that currently we build cache for 500 contact records at a time, hence below approach
1063 $insertValues = array();
1064 while($dao->fetch()) {
d897dad5 1065 $insertValues[] = "('civicrm_contact', {$dao->contact_id}, {$dao->contact_id}, '{$cacheKey}', '" . CRM_Core_DAO::escapeString($dao->sort_name) . "')";
778a10cb 1066 }
1067
1068 //update pre/next cache using single insert query
1069 if (!empty($insertValues)) {
1070 $sql = 'INSERT INTO civicrm_prevnext_cache ( entity_table, entity_id1, entity_id2, cacheKey, data ) VALUES
1071'.implode(',', $insertValues);
1072
1073 $result = CRM_Core_DAO::executeQuery($sql);
1074 }
1075 }
1076
6a488035
TO
1077 /**
1078 * Given the current formValues, gets the query in local
1079 * language
1080 *
1081 * @param array(
1082 reference) $formValues submitted formValues
1083 *
1084 * @return array $qill which contains an array of strings
1085 * @access public
1086 */
1087
1088 // the current internationalisation is bad, but should more or less work
1089 // for most of "European" languages
1090 public function getQILL() {
1091 return $this->_query->qill();
1092 }
1093
1094 /**
100fef9d 1095 * Name of export file.
6a488035
TO
1096 *
1097 * @param string $output type of output
1098 *
1099 * @return string name of the file
1100 */
1101 function getExportFileName($output = 'csv') {
1102 return ts('CiviCRM Contact Search');
1103 }
1104
1105 /**
100fef9d 1106 * Get colunmn headers for search selector
6a488035
TO
1107 *
1108 *
1109 * @return array $_columnHeaders
1110 * @access private
1111 */
1112 private static function &_getColumnHeaders() {
1113 if (!isset(self::$_columnHeaders)) {
1114 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
1115 'address_options', TRUE, NULL, TRUE
1116 );
1117
5defa2fd
KJ
1118 self::$_columnHeaders = array(
1119 'contact_type' => array('desc' => ts('Contact Type')),
6a488035
TO
1120 'sort_name' => array(
1121 'name' => ts('Name'),
1122 'sort' => 'sort_name',
1123 'direction' => CRM_Utils_Sort::ASCENDING,
1124 ),
1125 );
1126
5defa2fd
KJ
1127 $defaultAddress = array(
1128 'street_address' => array('name' => ts('Address')),
1129 'city' => array(
1130 'name' => ts('City'),
6a488035
TO
1131 'sort' => 'city',
1132 'direction' => CRM_Utils_Sort::DONTCARE,
1133 ),
5defa2fd
KJ
1134 'state_province' => array(
1135 'name' => ts('State'),
6a488035
TO
1136 'sort' => 'state_province',
1137 'direction' => CRM_Utils_Sort::DONTCARE,
1138 ),
5defa2fd
KJ
1139 'postal_code' => array(
1140 'name' => ts('Postal'),
6a488035
TO
1141 'sort' => 'postal_code',
1142 'direction' => CRM_Utils_Sort::DONTCARE,
1143 ),
5defa2fd
KJ
1144 'country' => array(
1145 'name' => ts('Country'),
6a488035
TO
1146 'sort' => 'country',
1147 'direction' => CRM_Utils_Sort::DONTCARE,
1148 ),
1149 );
1150
1151 foreach ($defaultAddress as $columnName => $column) {
a7488080 1152 if (!empty($addressOptions[$columnName])) {
6a488035
TO
1153 self::$_columnHeaders[$columnName] = $column;
1154 }
1155 }
1156
5defa2fd
KJ
1157 self::$_columnHeaders['email'] = array(
1158 'name' => ts('Email'),
6a488035
TO
1159 'sort' => 'email',
1160 'direction' => CRM_Utils_Sort::DONTCARE,
1161 );
1162
1163 self::$_columnHeaders['phone'] = array('name' => ts('Phone'));
1164 }
1165 return self::$_columnHeaders;
1166 }
1167
86538308
EM
1168 /**
1169 * @return CRM_Contact_BAO_Query
1170 */
6a488035
TO
1171 function &getQuery() {
1172 return $this->_query;
1173 }
1174
86538308
EM
1175 /**
1176 * @return CRM_Contact_DAO_Contact
1177 */
6a488035
TO
1178 function alphabetQuery() {
1179 return $this->_query->searchQuery(NULL, NULL, NULL, FALSE, FALSE, TRUE);
1180 }
1181
86538308 1182 /**
c490a46a 1183 * @param array $params
86538308 1184 * @param $action
100fef9d 1185 * @param int $sortID
86538308
EM
1186 * @param null $displayRelationshipType
1187 * @param string $queryOperator
1188 *
1189 * @return CRM_Contact_DAO_Contact
1190 */
6a488035
TO
1191 function contactIDQuery($params, $action, $sortID, $displayRelationshipType = NULL, $queryOperator = 'AND') {
1192 $sortOrder = &$this->getSortOrder($this->_action);
1193 $sort = new CRM_Utils_Sort($sortOrder, $sortID);
1194
1195 // rectify params to what proximity search expects if there is a value for prox_distance
1196 // CRM-7021 CRM-7905
1197 if (!empty($params)) {
1198 CRM_Contact_BAO_ProximityQuery::fixInputParams($params);
1199 }
1200
1201 if (!$displayRelationshipType) {
1202 $query = new CRM_Contact_BAO_Query($params,
1203 $this->_returnProperties,
1204 NULL, FALSE, FALSE, 1,
1205 FALSE, TRUE, TRUE, NULL,
1206 $queryOperator
1207 );
1208 }
1209 else {
1210 $query = new CRM_Contact_BAO_Query($params, $this->_returnProperties,
1211 NULL, FALSE, FALSE, 1,
1212 FALSE, TRUE, TRUE, $displayRelationshipType,
1213 $queryOperator
1214 );
1215 }
1216 $value = $query->searchQuery(0, 0, $sort,
1217 FALSE, FALSE, FALSE,
1218 FALSE, FALSE
1219 );
1220 return $value;
1221 }
1222
86538308
EM
1223 /**
1224 * @param $returnProperties
1225 *
1226 * @return array
1227 */
6a488035
TO
1228 function &makeProperties(&$returnProperties) {
1229 $properties = array();
1230 foreach ($returnProperties as $name => $value) {
1231 if ($name != 'location') {
c6ff5b0d 1232 // special handling for group and tag
fd18baa6
KJ
1233 if (in_array($name, array('group', 'tag'))) {
1234 $name = "{$name}s";
1235 }
c6ff5b0d
KJ
1236
1237 // special handling for notes
1238 if (in_array($name, array('note', 'note_subject', 'note_body'))) {
1239 $name = "notes";
1240 }
1241
6a488035
TO
1242 $properties[] = $name;
1243 }
1244 else {
1245 // extract all the location stuff
1246 foreach ($value as $n => $v) {
1247 foreach ($v as $n1 => $v1) {
1248 if (!strpos('_id', $n1) && $n1 != 'location_type') {
1249 $properties[] = "{$n}-{$n1}";
1250 }
1251 }
1252 }
1253 }
1254 }
1255 return $properties;
1256 }
1257}
6a488035 1258