backport 24259
[civicrm-core.git] / CRM / Core / BAO / UFMatch.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * The basic class that interfaces with the external user framework.
20 */
21 class CRM_Core_BAO_UFMatch extends CRM_Core_DAO_UFMatch {
22
23 /**
24 * Create UF Match, Note that this function is here in it's simplest form @ the moment
25 *
26 * @param $params
27 *
28 * @return \CRM_Core_DAO_UFMatch
29 */
30 public static function create($params) {
31 $hook = empty($params['id']) ? 'create' : 'edit';
32 CRM_Utils_Hook::pre($hook, 'UFMatch', CRM_Utils_Array::value('id', $params), $params);
33 if (empty($params['domain_id'])) {
34 $params['domain_id'] = CRM_Core_Config::domainID();
35 }
36 $dao = new CRM_Core_DAO_UFMatch();
37 $dao->copyValues($params);
38 // Fixme: this function cannot update records
39 if (!$dao->find(TRUE)) {
40 $dao->save();
41 Civi::$statics[__CLASS__][$params['domain_id']][(int) $dao->contact_id] = (int) $dao->uf_id;
42 CRM_Utils_Hook::post($hook, 'UFMatch', $dao->id, $dao);
43 }
44 return $dao;
45 }
46
47 /**
48 * Given a UF user object, make sure there is a contact
49 * object for this user. If the user has new values, we need
50 * to update the CRM DB with the new values
51 *
52 * @param Object $user
53 * The drupal user object.
54 * @param bool $update
55 * Has the user object been edited.
56 * @param $uf
57 *
58 * @param $ctype
59 * @param bool $isLogin
60 *
61 * @throws CRM_Core_Exception
62 */
63 public static function synchronize(&$user, $update, $uf, $ctype, $isLogin = FALSE) {
64 $userSystem = CRM_Core_Config::singleton()->userSystem;
65 $session = CRM_Core_Session::singleton();
66 if (!is_object($session)) {
67 throw new CRM_Core_Exception('wow, session is not an object?');
68 return;
69 }
70
71 $userSystemID = $userSystem->getBestUFID($user);
72 $uniqId = $userSystem->getBestUFUniqueIdentifier($user);
73
74 // if the id of the object is zero (true for anon users in drupal)
75 // have we already processed this user, if so early
76 // return.
77 $userID = $session->get('userID');
78 $ufID = $session->get('ufID');
79
80 if (!$update && $ufID == $userSystemID) {
81 return;
82 }
83
84 //check do we have logged in user.
85 $isUserLoggedIn = CRM_Utils_System::isUserLoggedIn();
86
87 // reset the session if we are a different user
88 if ($ufID && $ufID != $userSystemID) {
89 $session->reset();
90
91 //get logged in user ids, and set to session.
92 if ($isUserLoggedIn) {
93 $userIds = self::getUFValues();
94 $session->set('ufID', CRM_Utils_Array::value('uf_id', $userIds, ''));
95 $session->set('userID', CRM_Utils_Array::value('contact_id', $userIds, ''));
96 }
97 }
98
99 // return early
100 if ($userSystemID == 0) {
101 return;
102 }
103
104 $ufmatch = self::synchronizeUFMatch($user, $userSystemID, $uniqId, $uf, NULL, $ctype, $isLogin);
105 if (!$ufmatch) {
106 return;
107 }
108
109 //make sure we have session w/ consistent ids.
110 $ufID = $ufmatch->uf_id;
111 $userID = $ufmatch->contact_id;
112 if ($isUserLoggedIn) {
113 $loggedInUserUfID = CRM_Utils_System::getLoggedInUfID();
114 //are we processing logged in user.
115 if ($loggedInUserUfID && $loggedInUserUfID != $ufID) {
116 $userIds = self::getUFValues($loggedInUserUfID);
117 $ufID = CRM_Utils_Array::value('uf_id', $userIds, '');
118 $userID = CRM_Utils_Array::value('contact_id', $userIds, '');
119 }
120 }
121
122 //set user ids to session.
123 $session->set('ufID', $ufID);
124 $session->set('userID', $userID);
125
126 // add current contact to recently viewed
127 if ($ufmatch->contact_id) {
128 list($displayName, $contactImage, $contactType, $contactSubtype, $contactImageUrl)
129 = CRM_Contact_BAO_Contact::getDisplayAndImage($ufmatch->contact_id, TRUE, TRUE);
130
131 $otherRecent = [
132 'imageUrl' => $contactImageUrl,
133 'subtype' => $contactSubtype,
134 'editUrl' => CRM_Utils_System::url('civicrm/contact/add', "reset=1&action=update&cid={$ufmatch->contact_id}"),
135 ];
136
137 CRM_Utils_Recent::add($displayName,
138 CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$ufmatch->contact_id}"),
139 $ufmatch->contact_id,
140 $contactType,
141 $ufmatch->contact_id,
142 $displayName,
143 $otherRecent
144 );
145 }
146 }
147
148 /**
149 * Synchronize the object with the UF Match entry. Can be called stand-alone from
150 * the drupalUsers script
151 *
152 * @param Object $user
153 * The drupal user object.
154 * @param string $userKey
155 * The id of the user from the uf object.
156 * @param string $uniqId
157 * The OpenID of the user.
158 * @param string $uf
159 * The name of the user framework.
160 * @param int $status
161 * Returns the status if user created or already exits (used for CMS sync).
162 * @param string $ctype
163 * contact type
164 * @param bool $isLogin
165 *
166 * @return CRM_Core_DAO_UFMatch|bool
167 */
168 public static function &synchronizeUFMatch(&$user, $userKey, $uniqId, $uf, $status = NULL, $ctype = NULL, $isLogin = FALSE) {
169 $config = CRM_Core_Config::singleton();
170 $newContact = FALSE;
171
172 // make sure that a contact id exists for this user id
173 $ufmatch = new CRM_Core_DAO_UFMatch();
174 $ufmatch->domain_id = CRM_Core_Config::domainID();
175 $ufmatch->uf_id = $userKey;
176
177 if (!$ufmatch->find(TRUE)) {
178 $transaction = new CRM_Core_Transaction();
179
180 $dao = NULL;
181 if (!empty($_POST) && !$isLogin) {
182 $params = $_POST;
183 $params['email'] = $uniqId;
184 // dev/core#1858 Ensure that if we have a contactID parameter which is set in the Create user Record contact task form
185 // That this contacID value is passed through as the contact_id to the get duplicate contacts function. This is necessary because for Drupal 8 this function gets invoked
186 // Before the civicrm_uf_match record is added where as in D7 it isn't called until the user tries to actually login.
187 if (!empty($params['contactID'])) {
188 $params['contact_id'] = $params['contactID'];
189 }
190
191 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($params, 'Individual', 'Unsupervised', [], FALSE);
192
193 if (!empty($ids) && Civi::settings()->get('uniq_email_per_site')) {
194 // restrict dupeIds to ones that belong to current domain/site.
195 $siteContacts = CRM_Core_BAO_Domain::getContactList();
196 foreach ($ids as $index => $dupeId) {
197 if (!in_array($dupeId, $siteContacts)) {
198 unset($ids[$index]);
199 }
200 }
201 // re-index the array
202 $ids = array_values($ids);
203 }
204 if (!empty($ids)) {
205 $dao = new CRM_Core_DAO();
206 $dao->contact_id = $ids[0];
207 }
208 }
209 else {
210 $dao = CRM_Contact_BAO_Contact::matchContactOnEmail($uniqId, $ctype);
211 }
212
213 $found = FALSE;
214 if ($dao) {
215 // ensure there does not exists a contact_id / uf_id pair
216 // in the DB. This might be due to multiple emails per contact
217 // CRM-9091
218 $sql = "
219 SELECT id
220 FROM civicrm_uf_match
221 WHERE contact_id = %1
222 AND domain_id = %2
223 ";
224 $params = [
225 1 => [$dao->contact_id, 'Integer'],
226 2 => [CRM_Core_Config::domainID(), 'Integer'],
227 ];
228 $conflict = CRM_Core_DAO::singleValueQuery($sql, $params);
229
230 if (!$conflict) {
231 $found = TRUE;
232 $ufmatch->contact_id = $dao->contact_id;
233 $ufmatch->uf_name = $uniqId;
234 }
235 }
236
237 if (!$found) {
238 // Not sure why we're testing for this. Is there ever a case
239 // in which $user is not an object?
240 if (is_object($user)) {
241 if ($config->userSystem->is_drupal) {
242 $primary_email = $uniqId;
243 }
244 elseif ($uf === 'WordPress') {
245 $primary_email = $user->user_email;
246 }
247 else {
248 $primary_email = $user->email;
249 }
250 $params['email'] = $primary_email;
251 }
252
253 if ($ctype === 'Organization') {
254 $params['organization_name'] = $uniqId;
255 }
256 elseif ($ctype === 'Household') {
257 $params['household_name'] = $uniqId;
258 }
259
260 $params['contact_type'] = $ctype ?? 'Individual';
261
262 // extract first / middle / last name
263 // for joomla
264 if ($uf === 'Joomla' && $user->name) {
265 CRM_Utils_String::extractName($user->name, $params);
266 }
267
268 if ($uf === 'WordPress') {
269 if ($user->first_name) {
270 $params['first_name'] = $user->first_name;
271 }
272
273 if ($user->last_name) {
274 $params['last_name'] = $user->last_name;
275 }
276 }
277
278 $contactId = civicrm_api3('Contact', 'create', $params)['id'];
279 $ufmatch->contact_id = $contactId;
280 $ufmatch->uf_name = $uniqId;
281 }
282
283 // check that there are not two CMS IDs matching the same CiviCRM contact - this happens when a civicrm
284 // user has two e-mails and there is a cms match for each of them
285 // the gets rid of the nasty fata error but still reports the error
286 $sql = "
287 SELECT uf_id
288 FROM civicrm_uf_match
289 WHERE ( contact_id = %1
290 OR uf_name = %2
291 OR uf_id = %3 )
292 AND domain_id = %4
293 ";
294 $params = [
295 1 => [$ufmatch->contact_id, 'Integer'],
296 2 => [$ufmatch->uf_name, 'String'],
297 3 => [$ufmatch->uf_id, 'Integer'],
298 4 => [$ufmatch->domain_id, 'Integer'],
299 ];
300
301 $conflict = CRM_Core_DAO::singleValueQuery($sql, $params);
302
303 if (!$conflict) {
304 $ufmatch = CRM_Core_BAO_UFMatch::create((array) $ufmatch);
305 $newContact = TRUE;
306 $transaction->commit();
307 }
308 else {
309 $msg = ts("Contact ID %1 is a match for %2 user %3 but has already been matched to %4",
310 [
311 1 => $ufmatch->contact_id,
312 2 => $uf,
313 3 => $ufmatch->uf_id,
314 4 => $conflict,
315 ]
316 );
317 unset($conflict);
318 }
319 }
320
321 if ($status) {
322 return $newContact;
323 }
324 else {
325 return $ufmatch;
326 }
327 }
328
329 /**
330 * Update the uf_name in the user object.
331 *
332 * @param int $contactId
333 * Id of the contact to update.
334 */
335 public static function updateUFName($contactId) {
336 if (!Civi::settings()->get('syncCMSEmail') || !$contactId) {
337 return;
338 }
339
340 // 1.do check for contact Id.
341 $ufmatch = new CRM_Core_DAO_UFMatch();
342 $ufmatch->contact_id = $contactId;
343 $ufmatch->domain_id = CRM_Core_Config::domainID();
344 if (!$ufmatch->find(TRUE)) {
345 return;
346 }
347
348 $config = CRM_Core_Config::singleton();
349 $ufName = CRM_Contact_BAO_Contact::getPrimaryEmail($contactId);
350
351 if (!$ufName) {
352 return;
353 }
354
355 $update = FALSE;
356
357 if ($ufmatch->uf_name != $ufName) {
358 $update = TRUE;
359 }
360
361 // CRM-6928
362 // 2.do check for duplicate ufName.
363 $ufDupeName = new CRM_Core_DAO_UFMatch();
364 $ufDupeName->uf_name = $ufName;
365 $ufDupeName->domain_id = CRM_Core_Config::domainID();
366 if ($ufDupeName->find(TRUE) &&
367 $ufDupeName->contact_id != $contactId
368 ) {
369 $update = FALSE;
370 }
371
372 if (!$update) {
373 return;
374 }
375
376 // save the updated ufmatch object
377 $ufmatch->uf_name = $ufName;
378 $ufmatch->save();
379 $config->userSystem->updateCMSName($ufmatch->uf_id, $ufName);
380 }
381
382 /**
383 * Update the email value for the contact and user profile.
384 *
385 * @param int $contactId
386 * Contact ID of the user.
387 * @param string $emailAddress
388 * Email to be modified for the user.
389 */
390 public static function updateContactEmail($contactId, $emailAddress) {
391 $strtolower = function_exists('mb_strtolower') ? 'mb_strtolower' : 'strtolower';
392 $emailAddress = $strtolower($emailAddress);
393
394 $ufmatch = new CRM_Core_DAO_UFMatch();
395 $ufmatch->contact_id = $contactId;
396 $ufmatch->domain_id = CRM_Core_Config::domainID();
397 if ($ufmatch->find(TRUE)) {
398 // Save the email in UF Match table
399 $ufmatch->uf_name = $emailAddress;
400 CRM_Core_BAO_UFMatch::create((array) $ufmatch);
401
402 // If CMS integration is disabled skip Civi email update if CMS user email is changed
403 if (Civi::settings()->get('syncCMSEmail') == FALSE) {
404 return;
405 }
406
407 //check if the primary email for the contact exists
408 //$contactDetails[1] - email
409 //$contactDetails[3] - email id
410 $contactDetails = CRM_Contact_BAO_Contact_Location::getEmailDetails($contactId);
411
412 if (trim($contactDetails[1])) {
413 //update if record is found but different
414 $emailID = $contactDetails[3];
415 if (trim($contactDetails[1]) != $emailAddress) {
416 civicrm_api3('Email', 'create', [
417 'id' => $emailID,
418 'email' => $emailAddress,
419 ]);
420 }
421 }
422 else {
423 //else insert a new email record
424 $result = civicrm_api3('Email', 'create', [
425 'contact_id' => $contactId,
426 'email' => $emailAddress,
427 'is_primary' => 1,
428 ]);
429 $emailID = $result['id'];
430 }
431
432 CRM_Core_BAO_Log::register($contactId,
433 'civicrm_email',
434 $emailID
435 );
436 }
437 }
438
439 /**
440 * Delete the object records that are associated with this cms user.
441 *
442 * @param int $ufID
443 * Id of the user to delete.
444 */
445 public static function deleteUser($ufID) {
446 $ufmatch = new CRM_Core_DAO_UFMatch();
447
448 $ufmatch->uf_id = $ufID;
449 $ufmatch->domain_id = $domainId = CRM_Core_Config::domainID();
450 $ufmatch->delete();
451
452 // Flush cache
453 Civi::$statics[__CLASS__][$domainId] = [];
454 }
455
456 /**
457 * Get the contact_id given a uf_id.
458 *
459 * @param int $ufID
460 * Id of UF for which related contact_id is required.
461 *
462 * @return int|null
463 * contact_id on success, null otherwise
464 */
465 public static function getContactId($ufID) {
466 if (!$ufID) {
467 return NULL;
468 }
469 $domainId = CRM_Core_Config::domainID();
470
471 if (!isset(Civi::$statics[__CLASS__][$domainId])) {
472 Civi::$statics[__CLASS__][$domainId] = [];
473 }
474 $contactId = array_search($ufID, Civi::$statics[__CLASS__][$domainId]);
475 if ($contactId) {
476 return $contactId;
477 }
478 $ufmatch = new CRM_Core_DAO_UFMatch();
479 $ufmatch->uf_id = $ufID;
480 $ufmatch->domain_id = $domainId;
481 if ($ufmatch->find(TRUE)) {
482 $contactId = (int) $ufmatch->contact_id;
483 Civi::$statics[__CLASS__][$domainId][$contactId] = (int) $ufID;
484 return $contactId;
485 }
486 return NULL;
487 }
488
489 /**
490 * Get the uf_id given a contact_id.
491 *
492 * @param int $contactID
493 * ID of the contact for which related uf_id is required.
494 *
495 * @return int|null
496 * uf_id of the given contact_id on success, null otherwise
497 */
498 public static function getUFId($contactID) {
499 if (!$contactID) {
500 return NULL;
501 }
502 $domainId = CRM_Core_Config::domainID();
503 $contactID = (int) $contactID;
504
505 if (empty(Civi::$statics[__CLASS__][$domainId]) || !array_key_exists($contactID, Civi::$statics[__CLASS__][$domainId])) {
506 Civi::$statics[__CLASS__][$domainId][$contactID] = NULL;
507 $ufmatch = new CRM_Core_DAO_UFMatch();
508 $ufmatch->contact_id = $contactID;
509 $ufmatch->domain_id = $domainId;
510 if ($ufmatch->find(TRUE)) {
511 Civi::$statics[__CLASS__][$domainId][$contactID] = (int) $ufmatch->uf_id;
512 }
513 }
514 return Civi::$statics[__CLASS__][$domainId][$contactID];
515 }
516
517 /**
518 * @deprecated
519 * @return bool
520 */
521 public static function isEmptyTable() {
522 CRM_Core_Error::deprecatedFunctionWarning('unused function to be removed');
523 $sql = "SELECT count(id) FROM civicrm_uf_match";
524 return CRM_Core_DAO::singleValueQuery($sql) > 0 ? FALSE : TRUE;
525 }
526
527 /**
528 * Get the list of contact_id.
529 *
530 * @deprecated
531 * @return int
532 * contact_id on success, null otherwise
533 */
534 public static function getContactIDs() {
535 CRM_Core_Error::deprecatedFunctionWarning('unused function to be removed');
536 $id = [];
537 $dao = new CRM_Core_DAO_UFMatch();
538 $dao->find();
539 while ($dao->fetch()) {
540 $id[] = $dao->contact_id;
541 }
542 return $id;
543 }
544
545 /**
546 * See if this user exists, and if so, if they're allowed to login
547 *
548 * @deprecated
549 * @param int $openId
550 *
551 * @return bool
552 * true if allowed to login, false otherwise
553 */
554 public static function getAllowedToLogin($openId) {
555 CRM_Core_Error::deprecatedFunctionWarning('unused function to be removed');
556 $ufmatch = new CRM_Core_DAO_UFMatch();
557 $ufmatch->uf_name = $openId;
558 $ufmatch->allowed_to_login = 1;
559 if ($ufmatch->find(TRUE)) {
560 return TRUE;
561 }
562 return FALSE;
563 }
564
565 /**
566 * Get the next unused uf_id value, since the standalone UF doesn't
567 * have id's (it uses OpenIDs, which go in a different field)
568 *
569 * @deprecated
570 * @return int
571 * next highest unused value for uf_id
572 */
573 public static function getNextUfIdValue() {
574 CRM_Core_Error::deprecatedFunctionWarning('unused function to be removed');
575 $query = "SELECT MAX(uf_id)+1 AS next_uf_id FROM civicrm_uf_match";
576 $dao = CRM_Core_DAO::executeQuery($query);
577 if ($dao->fetch()) {
578 $ufId = $dao->next_uf_id;
579 }
580
581 if (!isset($ufId)) {
582 $ufId = 1;
583 }
584 return $ufId;
585 }
586
587 /**
588 * @param $email
589 * @deprecated
590 * @return bool
591 */
592 public static function isDuplicateUser($email) {
593 CRM_Core_Error::deprecatedFunctionWarning('unused function to be removed');
594 $session = CRM_Core_Session::singleton();
595 $contactID = $session->get('userID');
596 if (!empty($email) && isset($contactID)) {
597 $dao = new CRM_Core_DAO_UFMatch();
598 $dao->uf_name = $email;
599 if ($dao->find(TRUE) && $contactID != $dao->contact_id) {
600 return TRUE;
601 }
602 }
603 return FALSE;
604 }
605
606 /**
607 * Get uf match values for given uf id or logged in user.
608 *
609 * @param int $ufID
610 * Uf id.
611 *
612 * @return array
613 * uf values.
614 */
615 public static function getUFValues($ufID = NULL) {
616 if (!$ufID) {
617 //get logged in user uf id.
618 $ufID = CRM_Utils_System::getLoggedInUfID();
619 }
620 if (!$ufID) {
621 return [];
622 }
623
624 if (!isset(Civi::$statics[__CLASS__][__FUNCTION__][$ufID])) {
625 $ufmatch = new CRM_Core_DAO_UFMatch();
626 $ufmatch->uf_id = $ufID;
627 $ufmatch->domain_id = CRM_Core_Config::domainID();
628 if ($ufmatch->find(TRUE)) {
629 Civi::$statics[__CLASS__][__FUNCTION__][$ufID] = [
630 'uf_id' => $ufmatch->uf_id,
631 'uf_name' => $ufmatch->uf_name,
632 'contact_id' => $ufmatch->contact_id,
633 'domain_id' => $ufmatch->domain_id,
634 ];
635 }
636 }
637 return Civi::$statics[__CLASS__][__FUNCTION__][$ufID] ?? NULL;
638 }
639
640 /**
641 * @inheritDoc
642 */
643 public function addSelectWhereClause() {
644 // Prevent default behavior of joining ACLs onto the contact_id field
645 $clauses = [];
646 CRM_Utils_Hook::selectWhereClause($this, $clauses);
647 return $clauses;
648 }
649
650 }