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