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