Merge pull request #15316 from mepps/order-campaign-dashboard
[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'];
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. modify submitted params and update it with shared contact address
806 // make sure you preserve specific form values like location type, is_primary_ is_billing, master_id
807 // CRM-10336: Also empty any fields from the existing address block if they don't exist in master (otherwise they will persist)
808 foreach ($values as $field => $submittedValue) {
809 if (!in_array($field, $skipFields)) {
810 if (isset($masterAddress->$field)) {
811 $values[$field] = $masterAddress->$field;
812 }
813 else {
814 $values[$field] = '';
815 }
816 }
817 }
818
819 //5. modify the params to include county_id if it exist in master contact.
820 // CRM-16152: This is a hack since QF does not submit disabled field.
821 if (!empty($masterAddress->county_id) && empty($values['county_id'])) {
822 $values['county_id'] = $masterAddress->county_id;
823 }
824 }
825 }
826
827 /**
828 * Get the list of contact name give address associated array.
829 *
830 * @param array $addresses
831 * Associated array of.
832 *
833 * @return array
834 * associated array of contact names
835 */
836 public static function getAddressShareContactNames(&$addresses) {
837 $contactNames = [];
838 // get the list of master id's for address
839 $masterAddressIds = [];
840 foreach ($addresses as $key => $addressValue) {
841 if (!empty($addressValue['master_id'])) {
842 $masterAddressIds[] = $addressValue['master_id'];
843 }
844 }
845
846 if (!empty($masterAddressIds)) {
847 $query = 'SELECT ca.id, cc.display_name, cc.id as cid, cc.is_deleted
848 FROM civicrm_contact cc
849 INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
850 WHERE ca.id IN ( ' . implode(',', $masterAddressIds) . ')';
851 $dao = CRM_Core_DAO::executeQuery($query);
852
853 while ($dao->fetch()) {
854 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$dao->cid}");
855 $contactNames[$dao->id] = [
856 'name' => "<a href='{$contactViewUrl}'>{$dao->display_name}</a>",
857 'is_deleted' => $dao->is_deleted,
858 'contact_id' => $dao->cid,
859 ];
860 }
861 }
862 return $contactNames;
863 }
864
865 /**
866 * Clear the contact cache so things are kosher. We started off being super aggressive with clearing
867 * caches, but are backing off from this with every release. Compromise between ease of coding versus
868 * performance versus being accurate at that very instant
869 *
870 * @param bool $isEmptyPrevNextTable
871 * Should the civicrm_prev_next table be cleared of any contact entries.
872 * This is currently done from import but not other places and would
873 * likely affect user experience in unexpected ways. Existing behaviour retained
874 * ... reluctantly.
875 */
876 public static function clearContactCaches($isEmptyPrevNextTable = FALSE) {
877 if (!CRM_Core_Config::isPermitCacheFlushMode()) {
878 return;
879 }
880 if ($isEmptyPrevNextTable) {
881 // These two calls are redundant in default deployments, but they're
882 // meaningful if "prevnext" is memory-backed.
883 Civi::service('prevnext')->deleteItem();
884 CRM_Core_BAO_PrevNextCache::deleteItem();
885 }
886 // clear acl cache if any.
887 CRM_ACL_BAO_Cache::resetCache();
888 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
889 }
890
891 /**
892 * @param array $params
893 *
894 * @throws Exception
895 */
896 public static function updateGreeting($params) {
897 $contactType = $params['ct'];
898 $greeting = $params['gt'];
899 $valueID = $id = CRM_Utils_Array::value('id', $params);
900 $force = CRM_Utils_Array::value('force', $params);
901 $limit = CRM_Utils_Array::value('limit', $params);
902
903 // if valueID is not passed use default value
904 if (!$valueID) {
905 $valueID = $id = self::defaultGreeting($contactType, $greeting);
906 }
907
908 $filter = [
909 'contact_type' => $contactType,
910 'greeting_type' => $greeting,
911 ];
912
913 $allGreetings = CRM_Core_PseudoConstant::greeting($filter);
914 $originalGreetingString = $greetingString = CRM_Utils_Array::value($valueID, $allGreetings);
915 if (!$greetingString) {
916 throw new CRM_Core_Exception(ts('Incorrect greeting value id %1, or no default greeting for this contact type and greeting type.', [1 => $valueID]));
917 }
918
919 // build return properties based on tokens
920 $greetingTokens = CRM_Utils_Token::getTokens($greetingString);
921 $tokens = CRM_Utils_Array::value('contact', $greetingTokens);
922 $greetingsReturnProperties = [];
923 if (is_array($tokens)) {
924 $greetingsReturnProperties = array_fill_keys(array_values($tokens), 1);
925 }
926
927 // Process ALL contacts only when force=1 or force=2 is passed. Else only contacts with NULL greeting or addressee value are updated.
928 $processAll = $processOnlyIdSet = FALSE;
929 if ($force == 1) {
930 $processAll = TRUE;
931 }
932 elseif ($force == 2) {
933 $processOnlyIdSet = TRUE;
934 }
935
936 //FIXME : apiQuery should handle these clause.
937 $filterContactFldIds = $filterIds = [];
938 $idFldName = $displayFldName = NULL;
939 if (in_array($greeting, CRM_Contact_BAO_Contact::$_greetingTypes)) {
940 $idFldName = $greeting . '_id';
941 $displayFldName = $greeting . '_display';
942 }
943
944 if ($idFldName) {
945 $queryParams = [1 => [$contactType, 'String']];
946
947 // if $force == 1 then update all contacts else only
948 // those with NULL greeting or addressee value CRM-9476
949 if ($processAll) {
950 $sql = "SELECT DISTINCT id, $idFldName FROM civicrm_contact WHERE contact_type = %1 ";
951 }
952 else {
953 $sql = "
954 SELECT DISTINCT id, $idFldName
955 FROM civicrm_contact
956 WHERE contact_type = %1
957 AND ({$idFldName} IS NULL
958 OR ( {$idFldName} IS NOT NULL AND ({$displayFldName} IS NULL OR {$displayFldName} = '')) )";
959 }
960
961 if ($limit) {
962 $sql .= " LIMIT 0, %2";
963 $queryParams += [2 => [$limit, 'Integer']];
964 }
965
966 $dao = CRM_Core_DAO::executeQuery($sql, $queryParams);
967 while ($dao->fetch()) {
968 $filterContactFldIds[$dao->id] = $dao->$idFldName;
969
970 if (!CRM_Utils_System::isNull($dao->$idFldName)) {
971 $filterIds[$dao->id] = $dao->$idFldName;
972 }
973 }
974 }
975
976 if (empty($filterContactFldIds)) {
977 $filterContactFldIds[] = 0;
978 }
979
980 // retrieve only required contact information
981 $extraParams[] = ['contact_type', '=', $contactType, 0, 0];
982 // we do token replacement in the replaceGreetingTokens hook
983 list($greetingDetails) = CRM_Utils_Token::getTokenDetails(array_keys($filterContactFldIds),
984 $greetingsReturnProperties,
985 FALSE, FALSE, $extraParams
986 );
987 // perform token replacement and build update SQL
988 $contactIds = [];
989 $cacheFieldQuery = "UPDATE civicrm_contact SET {$greeting}_display = CASE id ";
990 foreach ($greetingDetails as $contactID => $contactDetails) {
991 if (!$processAll &&
992 !array_key_exists($contactID, $filterContactFldIds)
993 ) {
994 continue;
995 }
996
997 if ($processOnlyIdSet && !array_key_exists($contactID, $filterIds)) {
998 continue;
999 }
1000
1001 if ($id) {
1002 $greetingString = $originalGreetingString;
1003 $contactIds[] = $contactID;
1004 }
1005 else {
1006 if ($greetingBuffer = CRM_Utils_Array::value($filterContactFldIds[$contactID], $allGreetings)) {
1007 $greetingString = $greetingBuffer;
1008 }
1009 }
1010
1011 self::processGreetingTemplate($greetingString, $contactDetails, $contactID, 'CRM_UpdateGreeting');
1012 $greetingString = CRM_Core_DAO::escapeString($greetingString);
1013 $cacheFieldQuery .= " WHEN {$contactID} THEN '{$greetingString}' ";
1014
1015 $allContactIds[] = $contactID;
1016 }
1017
1018 if (!empty($allContactIds)) {
1019 $cacheFieldQuery .= " ELSE {$greeting}_display
1020 END;";
1021 if (!empty($contactIds)) {
1022 // need to update greeting _id field.
1023 // reset greeting _custom
1024 $resetCustomGreeting = '';
1025 if ($valueID != 4) {
1026 $resetCustomGreeting = ", {$greeting}_custom = NULL ";
1027 }
1028
1029 $queryString = "
1030 UPDATE civicrm_contact
1031 SET {$greeting}_id = {$valueID}
1032 {$resetCustomGreeting}
1033 WHERE id IN (" . implode(',', $contactIds) . ")";
1034 CRM_Core_DAO::executeQuery($queryString);
1035 }
1036
1037 // now update cache field
1038 CRM_Core_DAO::executeQuery($cacheFieldQuery);
1039 }
1040 }
1041
1042 /**
1043 * Fetch the default greeting for a given contact type.
1044 *
1045 * @param string $contactType
1046 * Contact type.
1047 * @param string $greetingType
1048 * Greeting type.
1049 *
1050 * @return int|null
1051 */
1052 public static function defaultGreeting($contactType, $greetingType) {
1053 $contactTypeFilters = [
1054 'Individual' => 1,
1055 'Household' => 2,
1056 'Organization' => 3,
1057 ];
1058 if (!isset($contactTypeFilters[$contactType])) {
1059 return NULL;
1060 }
1061 $filter = $contactTypeFilters[$contactType];
1062
1063 $id = CRM_Core_OptionGroup::values($greetingType, NULL, NULL, NULL,
1064 " AND is_default = 1 AND (filter = {$filter} OR filter = 0)",
1065 'value'
1066 );
1067 if (!empty($id)) {
1068 return current($id);
1069 }
1070 }
1071
1072 /**
1073 * Get the tokens that will need to be resolved to populate the contact's greetings.
1074 *
1075 * @param array $contactParams
1076 *
1077 * @return array
1078 * Array of tokens. The ALL ke
1079 */
1080 public static function getTokensRequiredForContactGreetings($contactParams) {
1081 $tokens = [];
1082 foreach (['addressee', 'email_greeting', 'postal_greeting'] as $greeting) {
1083 $string = '';
1084 if (!empty($contactParams[$greeting . '_id'])) {
1085 $string = CRM_Core_PseudoConstant::getLabel('CRM_Contact_BAO_Contact', $greeting . '_id', $contactParams[$greeting . '_id']);
1086 }
1087 $string = isset($contactParams[$greeting . '_custom']) ? $contactParams[$greeting . '_custom'] : $string;
1088 if (empty($string)) {
1089 $tokens[$greeting] = [];
1090 }
1091 else {
1092 $tokens[$greeting] = CRM_Utils_Token::getTokens($string);
1093 }
1094 }
1095 $allTokens = array_merge_recursive($tokens['addressee'], $tokens['email_greeting'], $tokens['postal_greeting']);
1096 $tokens['all'] = $allTokens;
1097 return $tokens;
1098 }
1099
1100 /**
1101 * Process a greeting template string to produce the individualised greeting text.
1102 *
1103 * This works just like message templates for mailings:
1104 * the template is processed with the token substitution mechanism,
1105 * to supply the individual contact data;
1106 * and it is also processed with Smarty,
1107 * to allow for conditionals etc. based on the contact data.
1108 *
1109 * Note: We don't pass any variables to Smarty --
1110 * all variable data is inserted into the input string
1111 * by the token substitution mechanism,
1112 * before Smarty is invoked.
1113 *
1114 * @param string $templateString
1115 * The greeting template string with contact tokens + Smarty syntax.
1116 *
1117 * @param array $contactDetails
1118 * @param int $contactID
1119 * @param string $className
1120 */
1121 public static function processGreetingTemplate(&$templateString, $contactDetails, $contactID, $className) {
1122 CRM_Utils_Token::replaceGreetingTokens($templateString, $contactDetails, $contactID, $className, TRUE);
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 }