70655e2ba1b37f404d7aa1b53d92185dc331035d
[civicrm-core.git] / CRM / Contact / BAO / Contact / Utils.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 class CRM_Contact_BAO_Contact_Utils {
18
19 /**
20 * Given a contact type, get the contact image.
21 *
22 * @param string $contactType
23 * Contact type.
24 * @param bool $urlOnly
25 * If we need to return only image url.
26 * @param int $contactId
27 * Contact id.
28 * @param bool $addProfileOverlay
29 * If profile overlay class should be added.
30 *
31 * @return string
32 * @throws \CRM_Core_Exception
33 */
34 public static function getImage($contactType, $urlOnly = FALSE, $contactId = NULL, $addProfileOverlay = TRUE) {
35 static $imageInfo = [];
36
37 $contactType = CRM_Utils_Array::explodePadded($contactType);
38 $contactType = $contactType[0];
39
40 if (!array_key_exists($contactType, $imageInfo)) {
41 $imageInfo[$contactType] = [];
42
43 $typeInfo = [];
44 $params = ['name' => $contactType];
45 CRM_Contact_BAO_ContactType::retrieve($params, $typeInfo);
46
47 if (!empty($typeInfo['image_URL'])) {
48 $imageUrl = $typeInfo['image_URL'];
49 $config = CRM_Core_Config::singleton();
50
51 if (!preg_match("/^(\/|(http(s)?:)).+$/i", $imageUrl)) {
52 $imageUrl = $config->resourceBase . $imageUrl;
53 }
54 $imageInfo[$contactType]['image'] = "<div class=\"icon crm-icon {$typeInfo['name']}-icon\" style=\"background: url('{$imageUrl}')\" title=\"{$contactType}\"></div>";
55 $imageInfo[$contactType]['url'] = $imageUrl;
56 }
57 else {
58 if (!empty($typeInfo['parent_id'])) {
59 $type = CRM_Contact_BAO_ContactType::getBasicType($typeInfo['name']) . '-subtype';
60 }
61 else {
62 $type = $typeInfo['name'] ?? NULL;
63 }
64
65 // do not add title since it hides contact name
66 if ($addProfileOverlay) {
67 $imageInfo[$contactType]['image'] = "<div class=\"icon crm-icon {$type}-icon\"></div>";
68 }
69 else {
70 $imageInfo[$contactType]['image'] = "<div class=\"icon crm-icon {$type}-icon\" title=\"{$contactType}\"></div>";
71 }
72 $imageInfo[$contactType]['url'] = NULL;
73 }
74 }
75
76 if ($addProfileOverlay) {
77 static $summaryOverlayProfileId = NULL;
78 if (!$summaryOverlayProfileId) {
79 $summaryOverlayProfileId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFGroup', 'summary_overlay', 'id', 'name');
80 }
81
82 $profileURL = CRM_Utils_System::url('civicrm/profile/view',
83 "reset=1&gid={$summaryOverlayProfileId}&id={$contactId}&snippet=4&is_show_email_task=1"
84 );
85
86 $imageInfo[$contactType]['summary-link'] = '<a href="' . $profileURL . '" class="crm-summary-link">' . $imageInfo[$contactType]['image'] . '</a>';
87 }
88 else {
89 $imageInfo[$contactType]['summary-link'] = $imageInfo[$contactType]['image'];
90 }
91
92 return $urlOnly ? $imageInfo[$contactType]['url'] : $imageInfo[$contactType]['summary-link'];
93 }
94
95 /**
96 * Function check for mix contact ids(individual+household etc...)
97 *
98 * @param array $contactIds
99 * Array of contact ids.
100 *
101 * @return bool
102 * true if mix contact array else false
103 *
104 */
105 public static function checkContactType(&$contactIds) {
106 if (empty($contactIds)) {
107 return FALSE;
108 }
109
110 $idString = implode(',', $contactIds);
111 $query = "
112 SELECT count( DISTINCT contact_type )
113 FROM civicrm_contact
114 WHERE id IN ( $idString )
115 ";
116 $count = CRM_Core_DAO::singleValueQuery($query);
117 return $count > 1;
118 }
119
120 /**
121 * Generate a checksum for a $entityId of type $entityType
122 *
123 * @param int $entityId
124 * @param int $ts
125 * Timestamp that checksum was generated.
126 * @param int $live
127 * Life of this checksum in hours/ 'inf' for infinite.
128 * @param string $hash
129 * Contact hash, if sent, prevents a query in inner loop.
130 *
131 * @param string $entityType
132 * @param null $hashSize
133 *
134 * @return array
135 * ( $cs, $ts, $live )
136 * @throws \CRM_Core_Exception
137 */
138 public static function generateChecksum($entityId, $ts = NULL, $live = NULL, $hash = NULL, $entityType = 'contact', $hashSize = NULL) {
139 // return a warning message if we dont get a entityId
140 // this typically happens when we do a message preview
141 // or an anon mailing view - CRM-8298
142 if (!$entityId) {
143 return 'invalidChecksum';
144 }
145
146 if (!$hash) {
147 if ($entityType == 'contact') {
148 $hash = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
149 $entityId, 'hash'
150 );
151 }
152 elseif ($entityType == 'mailing') {
153 $hash = CRM_Core_DAO::getFieldValue('CRM_Mailing_DAO_Mailing',
154 $entityId, 'hash'
155 );
156 }
157 }
158
159 if (!$hash) {
160 $hash = md5(uniqid(rand(), TRUE));
161 if ($hashSize) {
162 $hash = substr($hash, 0, $hashSize);
163 }
164
165 if ($entityType == 'contact') {
166 CRM_Core_DAO::setFieldValue('CRM_Contact_DAO_Contact',
167 $entityId,
168 'hash', $hash
169 );
170 }
171 elseif ($entityType == 'mailing') {
172 CRM_Core_DAO::setFieldValue('CRM_Mailing_DAO_Mailing',
173 $entityId,
174 'hash', $hash
175 );
176 }
177 }
178
179 if (!$ts) {
180 $ts = time();
181 }
182
183 if (!$live) {
184 $days = Civi::settings()->get('checksum_timeout');
185 $live = 24 * $days;
186 }
187
188 $cs = md5("{$hash}_{$entityId}_{$ts}_{$live}");
189 return "{$cs}_{$ts}_{$live}";
190 }
191
192 /**
193 * Make sure the checksum is valid for the passed in contactID.
194 *
195 * @param int $contactID
196 * @param string $inputCheck
197 * Checksum to match against.
198 *
199 * @return bool
200 * true if valid, else false
201 *
202 * @throws \CRM_Core_Exception
203 */
204 public static function validChecksum($contactID, $inputCheck) {
205
206 $input = CRM_Utils_System::explode('_', $inputCheck, 3);
207
208 $inputCS = $input[0] ?? NULL;
209 $inputTS = $input[1] ?? NULL;
210 $inputLF = $input[2] ?? NULL;
211
212 $check = self::generateChecksum($contactID, $inputTS, $inputLF);
213 // Joomla_11 - If $inputcheck is null without explicitly casting to a string
214 // you get an error.
215 if (!hash_equals($check, (string) $inputCheck)) {
216 return FALSE;
217 }
218
219 // no life limit for checksum
220 if ($inputLF == 'inf') {
221 return TRUE;
222 }
223
224 // checksum matches so now check timestamp
225 $now = time();
226 return ($inputTS + ($inputLF * 60 * 60) >= $now);
227 }
228
229 /**
230 * Create Current employer relationship for a individual.
231 *
232 * @param int $contactID
233 * Contact id of the individual.
234 * @param $organization
235 * (id or name).
236 * @param int $previousEmployerID
237 * @param bool $newContact
238 *
239 * @throws \CRM_Core_Exception
240 * @throws \CiviCRM_API3_Exception
241 */
242 public static function createCurrentEmployerRelationship($contactID, $organization, $previousEmployerID = NULL, $newContact = FALSE) {
243 //if organization name is passed. CRM-15368,CRM-15547
244 if (!CRM_Utils_System::isNull($organization) && !is_numeric($organization)) {
245 $dupeIDs = CRM_Contact_BAO_Contact::getDuplicateContacts(['organization_name' => $organization], 'Organization', 'Unsupervised', [], FALSE);
246
247 if (is_array($dupeIDs) && !empty($dupeIDs)) {
248 // we should create relationship only w/ first org CRM-4193
249 foreach ($dupeIDs as $orgId) {
250 $organization = $orgId;
251 break;
252 }
253 }
254 else {
255 //create new organization
256 $newOrg = [
257 'contact_type' => 'Organization',
258 'organization_name' => $organization,
259 ];
260 $org = CRM_Contact_BAO_Contact::create($newOrg);
261 $organization = $org->id;
262 }
263 }
264
265 if ($organization && is_numeric($organization)) {
266 $cid = ['contact' => $contactID];
267
268 // get the relationship type id of "Employee of"
269 $relTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Employee of', 'id', 'name_a_b');
270 if (!$relTypeId) {
271 throw new CRM_Core_Exception(ts("You seem to have deleted the relationship type 'Employee of'"));
272 }
273
274 // create employee of relationship
275 $relationshipParams = [
276 'is_active' => TRUE,
277 'relationship_type_id' => $relTypeId . '_a_b',
278 'contact_check' => [$organization => TRUE],
279 ];
280 list($valid, $invalid, $duplicate, $saved, $relationshipIds)
281 = CRM_Contact_BAO_Relationship::legacyCreateMultiple($relationshipParams, $cid);
282
283 // In case we change employer, clean previous employer related records.
284 if (!$previousEmployerID && !$newContact) {
285 $previousEmployerID = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contactID, 'employer_id');
286 }
287 if ($previousEmployerID &&
288 $previousEmployerID != $organization
289 ) {
290 self::clearCurrentEmployer($contactID, $previousEmployerID);
291 }
292
293 // set current employer
294 self::setCurrentEmployer([$contactID => $organization]);
295
296 $relationshipParams['relationship_ids'] = $relationshipIds;
297 // Handle related memberships. CRM-3792
298 self::currentEmployerRelatedMembership($contactID, $organization, $relationshipParams, $duplicate, $previousEmployerID);
299 }
300 }
301
302 /**
303 * Create related memberships for current employer.
304 *
305 * @param int $contactID
306 * Contact id of the individual.
307 * @param int $employerID
308 * Contact id of the organization.
309 * @param array $relationshipParams
310 * Relationship params.
311 * @param bool $duplicate
312 * Are we triggered existing relationship.
313 *
314 * @param int $previousEmpID
315 *
316 * @throws CiviCRM_API3_Exception
317 * @throws \CRM_Core_Exception
318 */
319 public static function currentEmployerRelatedMembership($contactID, $employerID, $relationshipParams, $duplicate = FALSE, $previousEmpID = NULL) {
320 $ids = [];
321 $action = CRM_Core_Action::ADD;
322
323 //we do not know that triggered relationship record is active.
324 if ($duplicate) {
325 $relationship = new CRM_Contact_DAO_Relationship();
326 $relationship->contact_id_a = $contactID;
327 $relationship->contact_id_b = $employerID;
328 $relationship->relationship_type_id = $relationshipParams['relationship_type_id'];
329 if ($relationship->find(TRUE)) {
330 $action = CRM_Core_Action::UPDATE;
331 $ids['contact'] = $contactID;
332 $ids['contactTarget'] = $employerID;
333 $ids['relationship'] = $relationship->id;
334 CRM_Contact_BAO_Relationship::setIsActive($relationship->id, TRUE);
335 }
336 }
337
338 //need to handle related meberships. CRM-3792
339 if ($previousEmpID != $employerID) {
340 CRM_Contact_BAO_Relationship::relatedMemberships($contactID, $relationshipParams, $ids, $action);
341 }
342 }
343
344 /**
345 * Set current employer id and organization name.
346 *
347 * @param array $currentEmployerParams
348 * Associated array of contact id and its employer id.
349 */
350 public static function setCurrentEmployer($currentEmployerParams) {
351 foreach ($currentEmployerParams as $contactId => $orgId) {
352 $query = "UPDATE civicrm_contact contact_a,civicrm_contact contact_b
353 SET contact_a.employer_id=contact_b.id, contact_a.organization_name=contact_b.organization_name
354 WHERE contact_a.id ={$contactId} AND contact_b.id={$orgId}; ";
355 CRM_Core_DAO::executeQuery($query);
356 }
357 }
358
359 /**
360 * Update cached current employer name.
361 *
362 * @param int $organizationId
363 * Current employer id.
364 */
365 public static function updateCurrentEmployer($organizationId) {
366 $query = "UPDATE civicrm_contact contact_a,civicrm_contact contact_b
367 SET contact_a.organization_name=contact_b.organization_name
368 WHERE contact_a.employer_id=contact_b.id AND contact_b.id={$organizationId}; ";
369
370 CRM_Core_DAO::executeQuery($query);
371 }
372
373 /**
374 * Clear cached current employer name.
375 *
376 * @param int $contactId
377 * Contact id ( mostly individual contact id).
378 * @param int $employerId
379 * Contact id ( mostly organization contact id).
380 *
381 * @throws \CRM_Core_Exception
382 * @throws \CiviCRM_API3_Exception
383 */
384 public static function clearCurrentEmployer($contactId, $employerId = NULL) {
385 $query = "UPDATE civicrm_contact
386 SET organization_name=NULL, employer_id = NULL
387 WHERE id={$contactId}; ";
388
389 $dao = CRM_Core_DAO::executeQuery($query);
390
391 // need to handle related meberships. CRM-3792
392 if ($employerId) {
393 //1. disable corresponding relationship.
394 //2. delete related membership.
395
396 //get the relationship type id of "Employee of"
397 $relTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Employee of', 'id', 'name_a_b');
398 if (!$relTypeId) {
399 throw new CRM_Core_Exception(ts("You seem to have deleted the relationship type 'Employee of'"));
400 }
401 $relMembershipParams['relationship_type_id'] = $relTypeId . '_a_b';
402 $relMembershipParams['contact_check'][$employerId] = 1;
403
404 //get relationship id.
405 if (CRM_Contact_BAO_Relationship::checkDuplicateRelationship($relMembershipParams, $contactId, $employerId)) {
406 $relationship = new CRM_Contact_DAO_Relationship();
407 $relationship->contact_id_a = $contactId;
408 $relationship->contact_id_b = $employerId;
409 $relationship->relationship_type_id = $relTypeId;
410
411 if ($relationship->find(TRUE)) {
412 CRM_Contact_BAO_Relationship::setIsActive($relationship->id, FALSE);
413 CRM_Contact_BAO_Relationship::relatedMemberships($contactId, $relMembershipParams,
414 $ids = [],
415 CRM_Core_Action::DELETE
416 );
417 }
418 }
419 }
420 }
421
422 /**
423 * Build form for related contacts / on behalf of organization.
424 *
425 * @param CRM_Core_Form $form
426 * @param string $contactType
427 * contact type.
428 * @param int $countryID
429 * @param int $stateID
430 * @param string $title
431 * fieldset title.
432 *
433 * @throws \CiviCRM_API3_Exception
434 */
435 public static function buildOnBehalfForm(&$form, $contactType, $countryID, $stateID, $title) {
436 $form->assign('contact_type', $contactType);
437 $form->assign('fieldSetTitle', $title);
438 $form->assign('contactEditMode', TRUE);
439
440 $attributes = CRM_Core_DAO::getAttribute('CRM_Contact_DAO_Contact');
441 if ($form->_contactId) {
442 $form->assign('orgId', $form->_contactId);
443 }
444
445 switch ($contactType) {
446 case 'Organization':
447 $form->add('text', 'organization_name', ts('Organization Name'), $attributes['organization_name'], TRUE);
448 break;
449
450 case 'Household':
451 $form->add('text', 'household_name', ts('Household Name'), $attributes['household_name']);
452 break;
453
454 default:
455 // individual
456 $form->addElement('select', 'prefix_id', ts('Prefix'),
457 ['' => ts('- prefix -')] + CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id')
458 );
459 $form->addElement('text', 'first_name', ts('First Name'),
460 $attributes['first_name']
461 );
462 $form->addElement('text', 'middle_name', ts('Middle Name'),
463 $attributes['middle_name']
464 );
465 $form->addElement('text', 'last_name', ts('Last Name'),
466 $attributes['last_name']
467 );
468 $form->addElement('select', 'suffix_id', ts('Suffix'),
469 ['' => ts('- suffix -')] + CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id')
470 );
471 }
472
473 $addressSequence = CRM_Utils_Address::sequence(\Civi::settings()->get('address_format'));
474 $form->assign('addressSequence', array_fill_keys($addressSequence, 1));
475
476 //Primary Phone
477 $form->addElement('text',
478 'phone[1][phone]',
479 ts('Primary Phone'),
480 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Phone',
481 'phone'
482 )
483 );
484 //Primary Email
485 $form->addElement('text',
486 'email[1][email]',
487 ts('Primary Email'),
488 CRM_Core_DAO::getAttribute('CRM_Core_DAO_Email',
489 'email'
490 )
491 );
492 //build the address block
493 CRM_Contact_Form_Edit_Address::buildQuickForm($form);
494 }
495
496 /**
497 * Clear cache employer name and employer id
498 * of all employee when employer get deleted.
499 *
500 * @param int $employerId
501 * Contact id of employer ( organization id ).
502 */
503 public static function clearAllEmployee($employerId) {
504 $query = "
505 UPDATE civicrm_contact
506 SET organization_name=NULL, employer_id = NULL
507 WHERE employer_id={$employerId}; ";
508
509 $dao = CRM_Core_DAO::executeQuery($query);
510 }
511
512 /**
513 * Given an array of contact ids this function will return array with links to view contact page.
514 *
515 * @param array $contactIDs
516 * Associated contact id's.
517 * @param bool $addViewLink
518 * @param bool $addEditLink
519 * @param int $originalId
520 * Associated with the contact which is edited.
521 *
522 *
523 * @return array
524 * returns array with links to contact view
525 */
526 public static function formatContactIDSToLinks($contactIDs, $addViewLink = TRUE, $addEditLink = TRUE, $originalId = NULL) {
527 $contactLinks = [];
528 if (!is_array($contactIDs) || empty($contactIDs)) {
529 return $contactLinks;
530 }
531
532 // does contact has sufficient permissions.
533 $permissions = [
534 'view' => 'view all contacts',
535 'edit' => 'edit all contacts',
536 'merge' => 'merge duplicate contacts',
537 ];
538
539 $permissionedContactIds = [];
540 foreach ($permissions as $task => $permission) {
541 // give permission.
542 if (CRM_Core_Permission::check($permission)) {
543 foreach ($contactIDs as $contactId) {
544 $permissionedContactIds[$contactId][$task] = TRUE;
545 }
546 continue;
547 }
548
549 // check permission on acl basis.
550 if (in_array($task, [
551 'view',
552 'edit',
553 ])) {
554 $aclPermission = CRM_Core_Permission::VIEW;
555 if ($task == 'edit') {
556 $aclPermission = CRM_Core_Permission::EDIT;
557 }
558 foreach ($contactIDs as $contactId) {
559 if (CRM_Contact_BAO_Contact_Permission::allow($contactId, $aclPermission)) {
560 $permissionedContactIds[$contactId][$task] = TRUE;
561 }
562 }
563 }
564 }
565
566 // retrieve display names for all contacts
567 $query = '
568 SELECT c.id, c.display_name, c.contact_type, ce.email
569 FROM civicrm_contact c
570 LEFT JOIN civicrm_email ce ON ( ce.contact_id=c.id AND ce.is_primary = 1 )
571 WHERE c.id IN (' . implode(',', $contactIDs) . ' ) LIMIT 20';
572
573 $dao = CRM_Core_DAO::executeQuery($query);
574
575 $contactLinks['msg'] = NULL;
576 $i = 0;
577 while ($dao->fetch()) {
578
579 $contactLinks['rows'][$i]['display_name'] = $dao->display_name;
580 $contactLinks['rows'][$i]['primary_email'] = $dao->email;
581
582 // get the permission for current contact id.
583 $hasPermissions = $permissionedContactIds[$dao->id] ?? NULL;
584 if (!is_array($hasPermissions) || empty($hasPermissions)) {
585 $i++;
586 continue;
587 }
588
589 // do check for view.
590 if (array_key_exists('view', $hasPermissions)) {
591 $contactLinks['rows'][$i]['view'] = '<a class="action-item" href="' . CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $dao->id) . '" target="_blank">' . ts('View') . '</a>';
592 if (!$contactLinks['msg']) {
593 $contactLinks['msg'] = 'view';
594 }
595 }
596 if (array_key_exists('edit', $hasPermissions)) {
597 $contactLinks['rows'][$i]['edit'] = '<a class="action-item" href="' . CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $dao->id) . '" target="_blank">' . ts('Edit') . '</a>';
598 if (!$contactLinks['msg'] || $contactLinks['msg'] != 'merge') {
599 $contactLinks['msg'] = 'edit';
600 }
601 }
602 if (!empty($originalId) && array_key_exists('merge', $hasPermissions)) {
603 $rgBao = new CRM_Dedupe_BAO_DedupeRuleGroup();
604 $rgBao->contact_type = $dao->contact_type;
605 $rgBao->used = 'Supervised';
606 if ($rgBao->find(TRUE)) {
607 $rgid = $rgBao->id;
608 }
609 if ($rgid && isset($dao->id)) {
610 //get an url to merge the contact
611 $contactLinks['rows'][$i]['merge'] = '<a class="action-item" href="' . CRM_Utils_System::url('civicrm/contact/merge', "reset=1&cid=" . $originalId . '&oid=' . $dao->id . '&action=update&rgid=' . $rgid) . '">' . ts('Merge') . '</a>';
612 $contactLinks['msg'] = 'merge';
613 }
614 }
615
616 $i++;
617 }
618
619 return $contactLinks;
620 }
621
622 /**
623 * This function retrieve component related contact information.
624 *
625 * @param array $componentIds
626 * Array of component Ids.
627 * @param string $componentName
628 * @param array $returnProperties
629 * Array of return elements.
630 *
631 * @return array
632 * array of contact info.
633 */
634 public static function contactDetails($componentIds, $componentName, $returnProperties = []) {
635 $contactDetails = [];
636 if (empty($componentIds) ||
637 !in_array($componentName, ['CiviContribute', 'CiviMember', 'CiviEvent', 'Activity', 'CiviCase'])
638 ) {
639 return $contactDetails;
640 }
641
642 if (empty($returnProperties)) {
643 $autocompleteContactSearch = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
644 'contact_autocomplete_options'
645 );
646 $returnProperties = array_fill_keys(array_merge(['sort_name'],
647 array_keys($autocompleteContactSearch)
648 ), 1);
649 }
650
651 $compTable = NULL;
652 if ($componentName == 'CiviContribute') {
653 $compTable = 'civicrm_contribution';
654 }
655 elseif ($componentName == 'CiviMember') {
656 $compTable = 'civicrm_membership';
657 }
658 elseif ($componentName == 'Activity') {
659 $compTable = 'civicrm_activity';
660 $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
661 }
662 elseif ($componentName == 'CiviCase') {
663 $compTable = 'civicrm_case';
664 }
665 else {
666 $compTable = 'civicrm_participant';
667 }
668
669 $select = $from = [];
670 foreach ($returnProperties as $property => $ignore) {
671 $value = (in_array($property, [
672 'city',
673 'street_address',
674 'postal_code',
675 ])) ? 'address' : $property;
676 switch ($property) {
677 case 'sort_name':
678 if ($componentName == 'Activity') {
679 $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts);
680 $select[] = "contact.$property as $property";
681 $from[$value] = "
682 INNER JOIN civicrm_activity_contact acs ON (acs.activity_id = {$compTable}.id AND acs.record_type_id = {$sourceID})
683 INNER JOIN civicrm_contact contact ON ( contact.id = acs.contact_id )";
684 }
685 elseif ($componentName == 'CiviCase') {
686 $select[] = "contact.$property as $property";
687 $from[$value] = "
688 INNER JOIN civicrm_case_contact ccs ON (ccs.case_id = {$compTable}.id)
689 INNER JOIN civicrm_contact contact ON ( contact.id = ccs.contact_id )";
690 }
691 else {
692 $select[] = "$property as $property";
693 $from[$value] = "INNER JOIN civicrm_contact contact ON ( contact.id = $compTable.contact_id )";
694 }
695 break;
696
697 case 'target_sort_name':
698 $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
699 $select[] = "contact_target.sort_name as $property";
700 $from[$value] = "
701 INNER JOIN civicrm_activity_contact act ON (act.activity_id = {$compTable}.id AND act.record_type_id = {$targetID})
702 INNER JOIN civicrm_contact contact_target ON ( contact_target.id = act.contact_id )";
703 break;
704
705 case 'email':
706 case 'phone':
707 case 'city':
708 case 'street_address':
709 case 'postal_code':
710 $select[] = "$property as $property";
711 // Grab target contact properties if this is for activity
712 if ($componentName == 'Activity') {
713 $from[$value] = "LEFT JOIN civicrm_{$value} {$value} ON ( contact_target.id = {$value}.contact_id AND {$value}.is_primary = 1 ) ";
714 }
715 else {
716 $from[$value] = "LEFT JOIN civicrm_{$value} {$value} ON ( contact.id = {$value}.contact_id AND {$value}.is_primary = 1 ) ";
717 }
718 break;
719
720 case 'country':
721 case 'state_province':
722 $select[] = "{$property}.name as $property";
723 if (!in_array('address', $from)) {
724 // Grab target contact properties if this is for activity
725 if ($componentName == 'Activity') {
726 $from['address'] = 'LEFT JOIN civicrm_address address ON ( contact_target.id = address.contact_id AND address.is_primary = 1) ';
727 }
728 else {
729 $from['address'] = 'LEFT JOIN civicrm_address address ON ( contact.id = address.contact_id AND address.is_primary = 1) ';
730 }
731 }
732 $from[$value] = " LEFT JOIN civicrm_{$value} {$value} ON ( address.{$value}_id = {$value}.id ) ";
733 break;
734 }
735 }
736
737 //finally retrieve contact details.
738 if (!empty($select) && !empty($from)) {
739 $fromClause = implode(' ', $from);
740 $selectClause = implode(', ', $select);
741 $whereClause = "{$compTable}.id IN (" . implode(',', $componentIds) . ')';
742 $groupBy = CRM_Contact_BAO_Query::getGroupByFromSelectColumns($select, ["{$compTable}.id", 'contact.id']);
743
744 $query = "
745 SELECT contact.id as contactId, $compTable.id as componentId, $selectClause
746 FROM $compTable as $compTable $fromClause
747 WHERE $whereClause
748 {$groupBy}";
749
750 $contact = CRM_Core_DAO::executeQuery($query);
751 while ($contact->fetch()) {
752 $contactDetails[$contact->componentId]['contact_id'] = $contact->contactId;
753 foreach ($returnProperties as $property => $ignore) {
754 $contactDetails[$contact->componentId][$property] = $contact->$property;
755 }
756 }
757 }
758
759 return $contactDetails;
760 }
761
762 /**
763 * Function handles shared contact address processing.
764 * In this function we just modify submitted values so that new address created for the user
765 * has same address as shared contact address. We copy the address so that search etc will be
766 * much more efficient.
767 *
768 * @param array $address
769 * This is associated array which contains submitted form values.
770 */
771 public static function processSharedAddress(&$address) {
772 if (!is_array($address)) {
773 return;
774 }
775
776 // In create mode sharing a contact's address is pretty straight forward.
777 // In update mode we should check if the user stops sharing. If yes:
778 // - Set the master_id to an empty value
779 // Normal update process will automatically create new address with submitted values
780
781 // 1. loop through entire submitted address array
782 $skipFields = ['is_primary', 'location_type_id', 'is_billing', 'master_id', 'add_relationship', 'id', 'contact_id'];
783 foreach ($address as & $values) {
784 // 2. check if "Use another contact's address" is checked, if not continue
785 // Additionally, if master_id is set (address was shared), set master_id to empty value.
786 if (empty($values['use_shared_address'])) {
787 if (!empty($values['master_id'])) {
788 $values['master_id'] = '';
789 }
790 continue;
791 }
792
793 // Set add_relationship checkbox value
794 $values['add_relationship'] = !empty($values['add_relationship']);
795
796 // 3. get the address details for master_id
797 $masterAddress = new CRM_Core_BAO_Address();
798 $masterAddress->id = $values['master_id'] ?? NULL;
799 $masterAddress->find(TRUE);
800
801 // 4. CRM-10336: Empty all fields (execept the fields to skip)
802 foreach ($values as $field => $submittedValue) {
803 if (!in_array($field, $skipFields)) {
804 $values[$field] = '';
805 }
806 }
807
808 // 5. update address params to match shared address
809 // make sure you preserve specific form values like location type, is_primary_ is_billing, master_id
810 foreach ($masterAddress as $field => $value) {
811 if (!in_array($field, $skipFields)) {
812 if (isset($masterAddress->$field)) {
813 $values[$field] = $masterAddress->$field;
814 }
815 }
816 }
817 }
818 }
819
820 /**
821 * Get the list of contact name give address associated array.
822 *
823 * @param array $addresses
824 * Associated array of.
825 *
826 * @return array
827 * associated array of contact names
828 */
829 public static function getAddressShareContactNames(&$addresses) {
830 $contactNames = [];
831 // get the list of master id's for address
832 $masterAddressIds = [];
833 foreach ($addresses as $key => $addressValue) {
834 if (!empty($addressValue['master_id'])) {
835 $masterAddressIds[] = $addressValue['master_id'];
836 }
837 }
838
839 if (!empty($masterAddressIds)) {
840 $query = 'SELECT ca.id, cc.display_name, cc.id as cid, cc.is_deleted
841 FROM civicrm_contact cc
842 INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
843 WHERE ca.id IN ( ' . implode(',', $masterAddressIds) . ')';
844 $dao = CRM_Core_DAO::executeQuery($query);
845
846 while ($dao->fetch()) {
847 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$dao->cid}");
848 $contactNames[$dao->id] = [
849 'name' => "<a href='{$contactViewUrl}'>{$dao->display_name}</a>",
850 'is_deleted' => $dao->is_deleted,
851 'contact_id' => $dao->cid,
852 ];
853 }
854 }
855 return $contactNames;
856 }
857
858 /**
859 * Clear the contact cache so things are kosher. We started off being super aggressive with clearing
860 * caches, but are backing off from this with every release. Compromise between ease of coding versus
861 * performance versus being accurate at that very instant
862 *
863 * @param bool $isEmptyPrevNextTable
864 * Should the civicrm_prev_next table be cleared of any contact entries.
865 * This is currently done from import but not other places and would
866 * likely affect user experience in unexpected ways. Existing behaviour retained
867 * ... reluctantly.
868 */
869 public static function clearContactCaches($isEmptyPrevNextTable = FALSE): void {
870 if (!CRM_Core_Config::isPermitCacheFlushMode()) {
871 return;
872 }
873 if ($isEmptyPrevNextTable) {
874 // These two calls are redundant in default deployments, but they're
875 // meaningful if "prevnext" is memory-backed.
876 Civi::service('prevnext')->deleteItem();
877 CRM_Core_BAO_PrevNextCache::deleteItem();
878 }
879
880 CRM_ACL_BAO_Cache::opportunisticCacheFlush();
881 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
882 }
883
884 /**
885 * @param array $params
886 *
887 * @throws Exception
888 */
889 public static function updateGreeting($params) {
890 $contactType = $params['ct'];
891 $greeting = $params['gt'];
892 $valueID = $id = $params['id'] ?? NULL;
893 $force = $params['force'] ?? NULL;
894 $limit = $params['limit'] ?? NULL;
895
896 // if valueID is not passed use default value
897 if (!$valueID) {
898 $valueID = $id = self::defaultGreeting($contactType, $greeting);
899 }
900
901 $filter = [
902 'contact_type' => $contactType,
903 'greeting_type' => $greeting,
904 ];
905
906 $allGreetings = CRM_Core_PseudoConstant::greeting($filter);
907 $originalGreetingString = $greetingString = $allGreetings[$valueID] ?? NULL;
908 if (!$greetingString) {
909 throw new CRM_Core_Exception(ts('Incorrect greeting value id %1, or no default greeting for this contact type and greeting type.', [1 => $valueID]));
910 }
911
912 // build return properties based on tokens
913 $greetingTokens = CRM_Utils_Token::getTokens($greetingString);
914 $tokens = $greetingTokens['contact'] ?? NULL;
915 $greetingsReturnProperties = [];
916 if (is_array($tokens)) {
917 $greetingsReturnProperties = array_fill_keys(array_values($tokens), 1);
918 }
919
920 // Process ALL contacts only when force=1 or force=2 is passed. Else only contacts with NULL greeting or addressee value are updated.
921 $processAll = $processOnlyIdSet = FALSE;
922 if ($force == 1) {
923 $processAll = TRUE;
924 }
925 elseif ($force == 2) {
926 $processOnlyIdSet = TRUE;
927 }
928
929 //FIXME : apiQuery should handle these clause.
930 $filterContactFldIds = $filterIds = [];
931 $idFldName = $displayFldName = NULL;
932 if (in_array($greeting, CRM_Contact_BAO_Contact::$_greetingTypes)) {
933 $idFldName = $greeting . '_id';
934 $displayFldName = $greeting . '_display';
935 }
936
937 if ($idFldName) {
938 $queryParams = [1 => [$contactType, 'String']];
939
940 // if $force == 1 then update all contacts else only
941 // those with NULL greeting or addressee value CRM-9476
942 if ($processAll) {
943 $sql = "SELECT DISTINCT id, $idFldName FROM civicrm_contact WHERE contact_type = %1 ";
944 }
945 else {
946 $sql = "
947 SELECT DISTINCT id, $idFldName
948 FROM civicrm_contact
949 WHERE contact_type = %1
950 AND ({$idFldName} IS NULL
951 OR ( {$idFldName} IS NOT NULL AND ({$displayFldName} IS NULL OR {$displayFldName} = '')) )";
952 }
953
954 if ($limit) {
955 $sql .= " LIMIT 0, %2";
956 $queryParams += [2 => [$limit, 'Integer']];
957 }
958
959 $dao = CRM_Core_DAO::executeQuery($sql, $queryParams);
960 while ($dao->fetch()) {
961 $filterContactFldIds[$dao->id] = $dao->$idFldName;
962
963 if (!CRM_Utils_System::isNull($dao->$idFldName)) {
964 $filterIds[$dao->id] = $dao->$idFldName;
965 }
966 }
967 }
968
969 if (empty($filterContactFldIds)) {
970 $greetingDetails = [];
971 $filterContactFldIds[] = 0;
972 }
973 else {
974 // we do token replacement in the replaceGreetingTokens hook
975 [$greetingDetails] = CRM_Utils_Token::getTokenDetails(array_keys($filterContactFldIds), $greetingsReturnProperties, FALSE, FALSE);
976 }
977 // perform token replacement and build update SQL
978 $contactIds = [];
979 $cacheFieldQuery = "UPDATE civicrm_contact SET {$greeting}_display = CASE id ";
980 foreach ($greetingDetails as $contactID => $contactDetails) {
981 if (!$processAll &&
982 !array_key_exists($contactID, $filterContactFldIds)
983 ) {
984 continue;
985 }
986
987 if ($processOnlyIdSet && !array_key_exists($contactID, $filterIds)) {
988 continue;
989 }
990
991 if ($id) {
992 $greetingString = $originalGreetingString;
993 $contactIds[] = $contactID;
994 }
995 else {
996 if ($greetingBuffer = CRM_Utils_Array::value($filterContactFldIds[$contactID], $allGreetings)) {
997 $greetingString = $greetingBuffer;
998 }
999 }
1000
1001 self::processGreetingTemplate($greetingString, $contactDetails, $contactID, 'CRM_UpdateGreeting');
1002 $greetingString = CRM_Core_DAO::escapeString($greetingString);
1003 $cacheFieldQuery .= " WHEN {$contactID} THEN '{$greetingString}' ";
1004
1005 $allContactIds[] = $contactID;
1006 }
1007
1008 if (!empty($allContactIds)) {
1009 $cacheFieldQuery .= " ELSE {$greeting}_display
1010 END;";
1011 if (!empty($contactIds)) {
1012 // need to update greeting _id field.
1013 // reset greeting _custom
1014 $resetCustomGreeting = '';
1015 if ($valueID != 4) {
1016 $resetCustomGreeting = ", {$greeting}_custom = NULL ";
1017 }
1018
1019 $queryString = "
1020 UPDATE civicrm_contact
1021 SET {$greeting}_id = {$valueID}
1022 {$resetCustomGreeting}
1023 WHERE id IN (" . implode(',', $contactIds) . ")";
1024 CRM_Core_DAO::executeQuery($queryString);
1025 }
1026
1027 // now update cache field
1028 CRM_Core_DAO::executeQuery($cacheFieldQuery);
1029 }
1030 }
1031
1032 /**
1033 * Fetch the default greeting for a given contact type.
1034 *
1035 * @param string $contactType
1036 * Contact type.
1037 * @param string $greetingType
1038 * Greeting type.
1039 *
1040 * @return int|null
1041 */
1042 public static function defaultGreeting($contactType, $greetingType) {
1043 $contactTypeFilters = [
1044 'Individual' => 1,
1045 'Household' => 2,
1046 'Organization' => 3,
1047 ];
1048 if (!isset($contactTypeFilters[$contactType])) {
1049 return NULL;
1050 }
1051 $filter = $contactTypeFilters[$contactType];
1052
1053 $id = CRM_Core_OptionGroup::values($greetingType, NULL, NULL, NULL,
1054 " AND is_default = 1 AND (filter = {$filter} OR filter = 0)",
1055 'value'
1056 );
1057 if (!empty($id)) {
1058 return current($id);
1059 }
1060 }
1061
1062 /**
1063 * Get the tokens that will need to be resolved to populate the contact's greetings.
1064 *
1065 * @param array $contactParams
1066 *
1067 * @return array
1068 * Array of tokens. The ALL ke
1069 */
1070 public static function getTokensRequiredForContactGreetings($contactParams) {
1071 $tokens = [];
1072 foreach (['addressee', 'email_greeting', 'postal_greeting'] as $greeting) {
1073 $string = '';
1074 if (!empty($contactParams[$greeting . '_id'])) {
1075 $string = CRM_Core_PseudoConstant::getLabel('CRM_Contact_BAO_Contact', $greeting . '_id', $contactParams[$greeting . '_id']);
1076 }
1077 $string = $contactParams[$greeting . '_custom'] ?? $string;
1078 if (empty($string)) {
1079 $tokens[$greeting] = [];
1080 }
1081 else {
1082 $tokens[$greeting] = CRM_Utils_Token::getTokens($string);
1083 }
1084 }
1085 $allTokens = array_merge_recursive($tokens['addressee'], $tokens['email_greeting'], $tokens['postal_greeting']);
1086 $tokens['all'] = $allTokens;
1087 return $tokens;
1088 }
1089
1090 /**
1091 * Process a greeting template string to produce the individualised greeting text.
1092 *
1093 * This works just like message templates for mailings:
1094 * the template is processed with the token substitution mechanism,
1095 * to supply the individual contact data;
1096 * and it is also processed with Smarty,
1097 * to allow for conditionals etc. based on the contact data.
1098 *
1099 * Note: We don't pass any variables to Smarty --
1100 * all variable data is inserted into the input string
1101 * by the token substitution mechanism,
1102 * before Smarty is invoked.
1103 *
1104 * @param string $templateString
1105 * The greeting template string with contact tokens + Smarty syntax.
1106 *
1107 * @param array $contactDetails
1108 * @param int $contactID
1109 * @param string $className
1110 */
1111 public static function processGreetingTemplate(&$templateString, $contactDetails, $contactID, $className) {
1112 CRM_Utils_Token::replaceGreetingTokens($templateString, $contactDetails, $contactID, $className, TRUE);
1113 $templateString = CRM_Utils_String::parseOneOffStringThroughSmarty($templateString);
1114 }
1115
1116 /**
1117 * Determine if a contact ID is real/valid.
1118 *
1119 * @param int $contactId
1120 * The hypothetical contact ID
1121 *
1122 * @return bool
1123 * @throws \CRM_Core_Exception
1124 */
1125 public static function isContactId($contactId) {
1126 if ($contactId) {
1127 // ensure that this is a valid contact id (for session inconsistency rules)
1128 $cid = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
1129 $contactId,
1130 'id',
1131 'id'
1132 );
1133 if ($cid) {
1134 return TRUE;
1135 }
1136 }
1137 return FALSE;
1138 }
1139
1140 }