(NFC) Correct type hints for bad null default values
[civicrm-core.git] / CRM / Profile / Selector / Listings.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * This class is used to retrieve and display a range of
20 * contacts that match the given criteria (specifically for
21 * results of advanced search options.
22 *
23 */
24 class CRM_Profile_Selector_Listings extends CRM_Core_Selector_Base implements CRM_Core_Selector_API {
25
26 /**
27 * Array of supported links, currently view and edit
28 *
29 * @var array
30 */
31 public static $_links = NULL;
32
33 /**
34 * We use desc to remind us what that column is, name is used in the tpl
35 *
36 * @var array
37 */
38 public static $_columnHeaders;
39
40 /**
41 * The sql params we use to get the list of contacts.
42 *
43 * @var string
44 */
45 protected $_params;
46
47 /**
48 * The public visible fields to be shown to the user.
49 *
50 * @var array
51 */
52 protected $_fields;
53
54 /**
55 * The custom fields for this domain.
56 *
57 * @var array
58 */
59 protected $_customFields;
60
61 /**
62 * Cache the query object.
63 *
64 * @var object
65 */
66 protected $_query;
67
68 /**
69 * The group id that we are editing.
70 *
71 * @var int
72 */
73 protected $_gid;
74
75 /**
76 * Do we enable mapping of users.
77 *
78 * @var bool
79 */
80 protected $_map;
81
82 /**
83 * Do we enable edit link.
84 *
85 * @var bool
86 */
87 protected $_editLink;
88
89 /**
90 * Should we link to the UF Profile.
91 *
92 * @var bool
93 */
94 protected $_linkToUF;
95
96 /**
97 * Store profile ids if multiple profile ids are passed using comma separated.
98 * Currently lets implement this functionality only for dialog mode
99 * @var array
100 */
101 protected $_profileIds = [];
102
103 protected $_multiRecordTableName = NULL;
104
105 /**
106 * Class constructor.
107 *
108 * @param array $params the params for the where clause
109 * @param array $customFields
110 * @param array $ufGroupIds
111 * @param bool $map
112 * @param bool $editLink
113 * @param bool $linkToUF
114 *
115 * @return \CRM_Profile_Selector_Listings
116 */
117 public function __construct(
118 &$params,
119 &$customFields,
120 $ufGroupIds = NULL,
121 $map = FALSE,
122 $editLink = FALSE,
123 $linkToUF = FALSE
124 ) {
125 $this->_params = $params;
126
127 if (is_array($ufGroupIds)) {
128 $this->_profileIds = $ufGroupIds;
129 $this->_gid = $ufGroupIds[0];
130 }
131 else {
132 $this->_profileIds = [$ufGroupIds];
133 $this->_gid = $ufGroupIds;
134 }
135
136 $this->_map = $map;
137 $this->_editLink = $editLink;
138 $this->_linkToUF = $linkToUF;
139
140 //get the details of the uf group
141 if ($this->_gid) {
142 $groupId = CRM_Core_DAO::getFieldValue('CRM_Core_BAO_UFGroup',
143 $this->_gid, 'limit_listings_group_id'
144 );
145 }
146
147 // add group id to params if a uf group belong to a any group
148 if ($groupId) {
149 if (!empty($this->_params['group'])) {
150 $this->_params['group'][$groupId] = 1;
151 }
152 else {
153 $this->_params['group'] = [$groupId => 1];
154 }
155 }
156
157 $this->_fields = CRM_Core_BAO_UFGroup::getListingFields(CRM_Core_Action::VIEW,
158 CRM_Core_BAO_UFGroup::PUBLIC_VISIBILITY |
159 CRM_Core_BAO_UFGroup::LISTINGS_VISIBILITY,
160 FALSE, $this->_profileIds
161 );
162
163 $this->_customFields = &$customFields;
164
165 $returnProperties = CRM_Contact_BAO_Contact::makeHierReturnProperties($this->_fields);
166 $returnProperties['contact_type'] = 1;
167 $returnProperties['contact_sub_type'] = 1;
168 $returnProperties['sort_name'] = 1;
169
170 $queryParams = CRM_Contact_BAO_Query::convertFormValues($this->_params, 1);
171 $this->_query = new CRM_Contact_BAO_Query($queryParams, $returnProperties, $this->_fields);
172
173 //the below is done for query building for multirecord custom field listing
174 //to show all the custom field multi valued records of a particular contact
175 $this->setMultiRecordTableName($this->_fields);
176 }
177
178 /**
179 * This method returns the links that are given for each search row.
180 *
181 * @param bool $map
182 * @param bool $editLink
183 * @param bool $ufLink
184 * @param int[]|null $gids
185 *
186 * @return array
187 */
188 public static function &links($map = FALSE, $editLink = FALSE, $ufLink = FALSE, $gids = NULL) {
189 if (!self::$_links) {
190 self::$_links = [];
191
192 $viewPermission = TRUE;
193 if ($gids) {
194 // check view permission for each profile id, in case multiple profile ids are rendered
195 // then view action is disabled if any profile returns false
196 foreach ($gids as $profileId) {
197 $viewPermission = CRM_Core_Permission::ufGroupValid($profileId, CRM_Core_Permission::VIEW);
198 if (!$viewPermission) {
199 break;
200 }
201 }
202 }
203
204 if ($viewPermission) {
205 self::$_links[CRM_Core_Action::VIEW] = [
206 'name' => ts('View'),
207 'url' => 'civicrm/profile/view',
208 'qs' => 'reset=1&id=%%id%%&gid=%%gid%%',
209 'title' => ts('View Profile Details'),
210 ];
211 }
212
213 if ($editLink) {
214 self::$_links[CRM_Core_Action::UPDATE] = [
215 'name' => ts('Edit'),
216 'url' => 'civicrm/profile/edit',
217 'qs' => 'reset=1&id=%%id%%&gid=%%gid%%',
218 'title' => ts('Edit'),
219 ];
220 }
221
222 if ($ufLink) {
223 self::$_links[CRM_Core_Action::PROFILE] = [
224 'name' => ts('Website Profile'),
225 'url' => 'user/%%ufID%%',
226 'qs' => ' ',
227 'title' => ts('View Website Profile'),
228 ];
229 }
230
231 if ($map) {
232 self::$_links[CRM_Core_Action::MAP] = [
233 'name' => ts('Map'),
234 'url' => 'civicrm/profile/map',
235 'qs' => 'reset=1&cid=%%id%%&gid=%%gid%%',
236 'title' => ts('Map'),
237 ];
238 }
239 }
240 return self::$_links;
241 }
242
243 /**
244 * Getter for array of the parameters required for creating pager.
245 *
246 * @param $action
247 * @param array $params
248 */
249 public function getPagerParams($action, &$params) {
250 $status = CRM_Utils_System::isNull($this->_multiRecordTableName) ? ts('Contact %%StatusMessage%%') : ts('Contact Multi Records %%StatusMessage%%');
251 $params['status'] = $status;
252 $params['csvString'] = NULL;
253 $params['rowCount'] = Civi::settings()->get('default_pager_size');
254
255 $params['buttonTop'] = 'PagerTopButton';
256 $params['buttonBottom'] = 'PagerBottomButton';
257 }
258
259 /**
260 * Returns the column headers as an array of tuples:
261 * (name, sortName (key to the sort array))
262 *
263 * @param string $action
264 * The action being performed.
265 * @param string $output
266 * What should the result set include (web/email/csv).
267 *
268 * @return array
269 * the column headers that need to be displayed
270 */
271 public function &getColumnHeaders($action = NULL, $output = NULL) {
272 static $skipFields = ['group', 'tag'];
273 $multipleFields = ['url'];
274 $direction = CRM_Utils_Sort::ASCENDING;
275 $empty = TRUE;
276 if (!isset(self::$_columnHeaders)) {
277 self::$_columnHeaders = [
278 ['name' => ''],
279 [
280 'name' => ts('Name'),
281 'sort' => 'sort_name',
282 'direction' => CRM_Utils_Sort::ASCENDING,
283 'field_name' => 'sort_name',
284 ],
285 ];
286
287 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
288
289 foreach ($this->_fields as $name => $field) {
290 // skip pseudo fields
291 if (substr($name, 0, 9) == 'phone_ext') {
292 continue;
293 }
294
295 if (!empty($field['in_selector']) &&
296 !in_array($name, $skipFields)
297 ) {
298
299 if (strpos($name, '-') !== FALSE) {
300 $value = explode('-', $name);
301 $fieldName = $value[0] ?? NULL;
302 $lType = $value[1] ?? NULL;
303 $type = $value[2] ?? NULL;
304
305 if (!in_array($fieldName, $multipleFields)) {
306 if ($lType == 'Primary') {
307 $locationTypeName = 1;
308 }
309 else {
310 $locationTypeName = $locationTypes[$lType];
311 }
312
313 if (in_array($fieldName, [
314 'phone',
315 'im',
316 'email',
317 ])) {
318 if ($type) {
319 $name = "`$locationTypeName-$fieldName-$type`";
320 }
321 else {
322 $name = "`$locationTypeName-$fieldName`";
323 }
324 }
325 else {
326 $name = "`$locationTypeName-$fieldName`";
327 }
328 }
329 else {
330 $name = "website-{$lType}-{$fieldName}";
331 }
332 }
333
334 self::$_columnHeaders[] = [
335 'name' => $field['title'],
336 'sort' => $name,
337 'direction' => $direction,
338 'field_name' => CRM_Core_BAO_UFField::isValidFieldName($name) ? $name : $fieldName,
339 ];
340
341 $direction = CRM_Utils_Sort::DONTCARE;
342 $empty = FALSE;
343 }
344 }
345
346 // if we don't have any valid columns, don't add the implicit ones
347 // this allows the template to check on emptiness of column headers
348 if ($empty) {
349 self::$_columnHeaders = [];
350 }
351 else {
352 self::$_columnHeaders[] = ['desc' => ts('Actions')];
353 }
354 }
355 return self::$_columnHeaders;
356 }
357
358 /**
359 * Returns total number of rows for the query.
360 *
361 * @param int $action
362 *
363 * @return int
364 * Total number of rows
365 */
366 public function getTotalCount($action) {
367 $additionalWhereClause = 'contact_a.is_deleted = 0';
368 $additionalFromClause = NULL;
369 $returnQuery = NULL;
370
371 if ($this->_multiRecordTableName &&
372 !array_key_exists($this->_multiRecordTableName, $this->_query->_whereTables)
373 ) {
374 $additionalFromClause = $this->_query->_tables[$this->_multiRecordTableName] ?? NULL;
375 $returnQuery = TRUE;
376 }
377
378 $countVal = $this->_query->searchQuery(0, 0, NULL, TRUE, NULL, NULL, NULL,
379 $returnQuery, $additionalWhereClause, NULL, $additionalFromClause
380 );
381
382 if (!$returnQuery) {
383 return $countVal;
384 }
385
386 if ($returnQuery) {
387 $sql = preg_replace('/DISTINCT/', '', $countVal);
388 return CRM_Core_DAO::singleValueQuery($sql);
389 }
390 }
391
392 /**
393 * Return the qill for this selector.
394 *
395 * @return string
396 */
397 public function getQill() {
398 return $this->_query->qill();
399 }
400
401 /**
402 * Returns all the rows in the given offset and rowCount.
403 *
404 * @param string $action
405 * The action being performed.
406 * @param int $offset
407 * The row number to start from.
408 * @param int $rowCount
409 * The number of rows to return.
410 * @param string $sort
411 * The sql string that describes the sort order.
412 * @param string $output
413 * What should the result set include (web/email/csv).
414 *
415 * @param string $extraWhereClause
416 *
417 * @return int
418 * the total number of rows for this action
419 */
420 public function &getRows($action, $offset, $rowCount, $sort, $output = NULL, $extraWhereClause = NULL) {
421
422 $multipleFields = ['url'];
423 //$sort object processing for location fields
424 if ($sort) {
425 $vars = $sort->_vars;
426 $varArray = [];
427 foreach ($vars as $key => $field) {
428 $field = $vars[$key];
429 $fieldArray = explode('-', $field['name']);
430 $fieldType = $fieldArray['2'] ?? NULL;
431 if (is_numeric(CRM_Utils_Array::value('1', $fieldArray))) {
432 if (!in_array($fieldType, $multipleFields)) {
433 $locationType = new CRM_Core_DAO_LocationType();
434 $locationType->id = $fieldArray[1];
435 $locationType->find(TRUE);
436 if ($fieldArray[0] == 'email' || $fieldArray[0] == 'im' || $fieldArray[0] == 'phone') {
437 $field['name'] = "`" . $locationType->name . "-" . $fieldArray[0] . "-1`";
438 }
439 else {
440 $field['name'] = "`" . $locationType->name . "-" . $fieldArray[0] . "`";
441 }
442 }
443 else {
444 $field['name'] = "`website-" . $fieldArray[1] . "-{$fieldType}`";
445 }
446 }
447 $varArray[$key] = $field;
448 }
449 $sort->_vars = $varArray;
450 }
451
452 $additionalWhereClause = 'contact_a.is_deleted = 0';
453
454 if ($extraWhereClause) {
455 $additionalWhereClause .= " AND {$extraWhereClause}";
456 }
457
458 $returnQuery = NULL;
459 if ($this->_multiRecordTableName) {
460 $returnQuery = TRUE;
461 }
462 $this->_query->_useGroupBy = TRUE;
463 $result = $this->_query->searchQuery($offset, $rowCount, $sort, NULL, NULL,
464 NULL, NULL, $returnQuery, $additionalWhereClause
465 );
466
467 if ($returnQuery) {
468 $resQuery = preg_replace('/GROUP BY contact_a.id[\s]+ORDER BY/', ' ORDER BY', $result);
469 $result = CRM_Core_DAO::executeQuery($resQuery);
470 }
471
472 // process the result of the query
473 $rows = [];
474
475 // check if edit is configured in profile settings
476 if ($this->_gid) {
477 $editLink = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFGroup', $this->_gid, 'is_edit_link');
478 }
479
480 //FIXME : make sure to handle delete separately. CRM-4418
481 $mask = CRM_Core_Action::mask([CRM_Core_Permission::getPermission()]);
482 if ($editLink && ($mask & CRM_Core_Permission::EDIT)) {
483 // do not allow edit for anon users in joomla frontend, CRM-4668
484 $config = CRM_Core_Config::singleton();
485 if (!$config->userFrameworkFrontend || CRM_Core_Session::getLoggedInContactID()) {
486 $this->_editLink = TRUE;
487 }
488 }
489 $links = self::links($this->_map, $this->_editLink, $this->_linkToUF, $this->_profileIds);
490
491 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
492
493 $names = [];
494 static $skipFields = ['group', 'tag'];
495
496 foreach ($this->_fields as $key => $field) {
497 // skip pseudo fields
498 if (substr($key, 0, 9) == 'phone_ext') {
499 continue;
500 }
501
502 if (!empty($field['in_selector']) &&
503 !in_array($key, $skipFields)
504 ) {
505 if (strpos($key, '-') !== FALSE) {
506 $value = explode('-', $key);
507 $fieldName = $value[0] ?? NULL;
508 $id = $value[1] ?? NULL;
509 $type = $value[2] ?? NULL;
510
511 if (!in_array($fieldName, $multipleFields)) {
512 $locationTypeName = NULL;
513 if (is_numeric($id)) {
514 $locationTypeName = $locationTypes[$id] ?? NULL;
515 }
516 else {
517 if ($id == 'Primary') {
518 $locationTypeName = 1;
519 }
520 }
521
522 if (!$locationTypeName) {
523 continue;
524 }
525 $locationTypeName = str_replace(' ', '_', $locationTypeName);
526 if (in_array($fieldName, [
527 'phone',
528 'im',
529 'email',
530 ])) {
531 if ($type) {
532 $names[] = "{$locationTypeName}-{$fieldName}-{$type}";
533 }
534 else {
535 $names[] = "{$locationTypeName}-{$fieldName}";
536 }
537 }
538 else {
539 $names[] = "{$locationTypeName}-{$fieldName}";
540 }
541 }
542 else {
543 $names[] = "website-{$id}-{$fieldName}";
544 }
545 }
546 elseif ($field['name'] == 'id') {
547 $names[] = 'contact_id';
548 }
549 else {
550 $names[] = $field['name'];
551 }
552 }
553 }
554
555 $multipleSelectFields = ['preferred_communication_method' => 1];
556 $multiRecordTableId = NULL;
557 if ($this->_multiRecordTableName) {
558 $multiRecordTableId = "{$this->_multiRecordTableName}_id";
559 }
560
561 // we need to determine of overlay profile should be shown
562 $showProfileOverlay = CRM_Core_BAO_UFGroup::showOverlayProfile();
563
564 while ($result->fetch()) {
565 $this->_query->convertToPseudoNames($result);
566
567 if (isset($result->country)) {
568 // the query returns the untranslated country name
569 $i18n = CRM_Core_I18n::singleton();
570 $result->country = $i18n->translate($result->country);
571 }
572 $row = [];
573 $empty = TRUE;
574 $row[] = CRM_Contact_BAO_Contact_Utils::getImage($result->contact_sub_type ? $result->contact_sub_type : $result->contact_type,
575 FALSE,
576 $result->contact_id,
577 $showProfileOverlay
578 );
579 if ($result->sort_name) {
580 $row[] = $result->sort_name;
581 $empty = FALSE;
582 }
583 else {
584 continue;
585 }
586
587 foreach ($names as $name) {
588 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($name)) {
589 $row[] = CRM_Core_BAO_CustomField::displayValue($result->$name,
590 $cfID,
591 $result->contact_id
592 );
593 }
594 elseif (substr($name, -4) == '-url' &&
595 !empty($result->$name)
596 ) {
597 $url = CRM_Utils_System::fixURL($result->$name);
598 $typeId = substr($name, 0, -4) . "-website_type_id";
599 $typeName = CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Website', 'website_type_id', $result->$typeId);
600 if ($typeName) {
601 $row[] = "<a href=\"$url\">{$result->$name} (${typeName})</a>";
602 }
603 else {
604 $row[] = "<a href=\"$url\">{$result->$name}</a>";
605 }
606 }
607 elseif ($name == 'preferred_language') {
608 $row[] = CRM_Core_PseudoConstant::getLabel('CRM_Contact_DAO_Contact', 'preferred_language', $result->$name);
609 }
610 elseif ($multipleSelectFields &&
611 array_key_exists($name, $multipleSelectFields)
612 ) {
613 $paramsNew = [$name => $result->$name];
614 $name = [$name => ['newName' => $name, 'groupName' => $name]];
615
616 CRM_Core_OptionGroup::lookupValues($paramsNew, $name, FALSE);
617 $row[] = $paramsNew[$key];
618 }
619 elseif (strpos($name, '-im')) {
620 if (!empty($result->$name)) {
621 $providerId = $name . "-provider_id";
622 $providerName = CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $result->$providerId);
623 $row[] = $result->$name . " ({$providerName})";
624 }
625 else {
626 $row[] = '';
627 }
628 }
629 elseif (strpos($name, '-phone-')) {
630 $phoneExtField = str_replace('phone', 'phone_ext', $name);
631 if (isset($result->$phoneExtField)) {
632 $row[] = $result->$name . " (" . $result->$phoneExtField . ")";
633 }
634 else {
635 $row[] = $result->$name;
636 }
637 }
638 elseif (in_array($name, [
639 'addressee',
640 'email_greeting',
641 'postal_greeting',
642 ])) {
643 $dname = $name . '_display';
644 $row[] = $result->$dname;
645 }
646 elseif (in_array($name, [
647 'birth_date',
648 'deceased_date',
649 ])) {
650 $row[] = CRM_Utils_Date::customFormat($result->$name);
651 }
652 elseif (isset($result->$name)) {
653 $row[] = $result->$name;
654 }
655 else {
656 $row[] = '';
657 }
658
659 if (!empty($result->$name)) {
660 $empty = FALSE;
661 }
662 }
663
664 $newLinks = $links;
665 $params = [
666 'id' => $result->contact_id,
667 'gid' => implode(',', $this->_profileIds),
668 ];
669
670 // pass record id param to view url for multi record view
671 if ($multiRecordTableId && $newLinks) {
672 if ($result->$multiRecordTableId) {
673 if ($newLinks[CRM_Core_Action::VIEW]['url'] == 'civicrm/profile/view') {
674 $newLinks[CRM_Core_Action::VIEW]['qs'] .= "&multiRecord=view&recordId=%%recordId%%&allFields=1";
675 $params['recordId'] = $result->$multiRecordTableId;
676 }
677 }
678 }
679
680 if ($this->_linkToUF) {
681 $ufID = CRM_Core_BAO_UFMatch::getUFId($result->contact_id);
682 if (!$ufID) {
683 unset($newLinks[CRM_Core_Action::PROFILE]);
684 }
685 else {
686 $params['ufID'] = $ufID;
687 }
688 }
689
690 $row[] = CRM_Core_Action::formLink($newLinks,
691 $mask,
692 $params,
693 ts('more'),
694 FALSE,
695 'profile.selector.row',
696 'Contact',
697 $result->contact_id
698 );
699
700 if (!$empty) {
701 $rows[] = $row;
702 }
703 }
704 return $rows;
705 }
706
707 /**
708 * Name of export file.
709 *
710 * @param string $output
711 * Type of output.
712 *
713 * @return string
714 * name of the file
715 */
716 public function getExportFileName($output = 'csv') {
717 return ts('CiviCRM Profile Listings');
718 }
719
720 /**
721 * Set the _multiRecordTableName to display the result set.
722 *
723 * (according to multi record custom field values).
724 *
725 * @param array $fields
726 */
727 public function setMultiRecordTableName($fields) {
728 $customGroupId = $multiRecordTableName = NULL;
729 $selectorSet = FALSE;
730
731 foreach ($fields as $field => $properties) {
732 if (!CRM_Core_BAO_CustomField::getKeyID($field)) {
733 continue;
734 }
735 if ($cgId = CRM_Core_BAO_CustomField::isMultiRecordField($field)) {
736 $customGroupId = CRM_Utils_System::isNull($customGroupId) ? $cgId : $customGroupId;
737
738 //if the field is submitted set multiRecordTableName
739 if ($customGroupId) {
740 $isSubmitted = FALSE;
741 foreach ($this->_query->_params as $key => $value) {
742 //check the query params 'where' element
743 if ($value[0] == $field) {
744 $isSubmitted = TRUE;
745 break;
746 }
747 }
748
749 if ($isSubmitted) {
750 $this->_multiRecordTableName
751 = $multiRecordTableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupId, 'table_name');
752 if ($multiRecordTableName) {
753 return;
754 }
755 }
756
757 if (!empty($properties['in_selector'])) {
758 $selectorSet = TRUE;
759 }
760 }
761 }
762 }
763
764 if (!isset($customGroupId) || !$customGroupId) {
765 return;
766 }
767
768 //if the field is in selector and not a searchable field
769 //get the proper custom value table name
770 if ($selectorSet) {
771 $this->_multiRecordTableName
772 = $multiRecordTableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupId, 'table_name');
773 }
774 } //func close
775
776 }