Merge pull request #3261 from atif-shaikh/CoreBugs
[civicrm-core.git] / CRM / Dedupe / Merger.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
32 * $Id$
33 *
34 */
35 class CRM_Dedupe_Merger {
36
37 // FIXME: consider creating a common structure with cidRefs() and eidRefs()
38 // FIXME: the sub-pages references by the URLs should
39 // be loaded dynamically on the merge form instead
40 static function relTables() {
41 static $relTables;
42
43 $config = CRM_Core_Config::singleton();
44 if ($config->userSystem->is_drupal) {
45 $userRecordUrl = CRM_Utils_System::url('user/%ufid');
46 $title = ts('%1 User: %2; user id: %3', array(1 => $config->userFramework, 2 => '$ufname', 3 => '$ufid'));
47 }
48 elseif ($config->userFramework == 'Joomla') {
49 $userRecordUrl = $config->userFrameworkVersion > 1.5 ? $config->userFrameworkBaseURL . "index.php?option=com_users&view=user&task=user.edit&id=" . '%ufid' : $config->userFrameworkBaseURL . "index2.php?option=com_users&view=user&task=edit&id[]=" . '%ufid';
50 $title = ts('%1 User: %2; user id: %3', array(1 => $config->userFramework, 2 => '$ufname', 3 => '$ufid'));
51 }
52
53 if (!$relTables) {
54 $relTables = array(
55 'rel_table_contributions' => array(
56 'title' => ts('Contributions'),
57 'tables' => array('civicrm_contribution', 'civicrm_contribution_recur', 'civicrm_contribution_soft'),
58 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=contribute'),
59 ),
60 'rel_table_contribution_page' => array(
61 'title' => ts('Contribution Pages'),
62 'tables' => array('civicrm_contribution_page'),
63 'url' => CRM_Utils_System::url('civicrm/admin/contribute', 'reset=1&cid=$cid'),
64 ),
65 'rel_table_memberships' => array(
66 'title' => ts('Memberships'),
67 'tables' => array('civicrm_membership', 'civicrm_membership_log', 'civicrm_membership_type'),
68 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=member'),
69 ),
70 'rel_table_participants' => array(
71 'title' => ts('Participants'),
72 'tables' => array('civicrm_participant'),
73 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=participant'),
74 ),
75 'rel_table_events' => array(
76 'title' => ts('Events'),
77 'tables' => array('civicrm_event'),
78 'url' => CRM_Utils_System::url('civicrm/event/manage', 'reset=1&cid=$cid'),
79 ),
80 'rel_table_activities' => array(
81 'title' => ts('Activities'),
82 'tables' => array('civicrm_activity', 'civicrm_activity_contact'),
83 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=activity'),
84 ),
85 'rel_table_relationships' => array(
86 'title' => ts('Relationships'),
87 'tables' => array('civicrm_relationship'),
88 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=rel'),
89 ),
90 'rel_table_custom_groups' => array(
91 'title' => ts('Custom Groups'),
92 'tables' => array('civicrm_custom_group'),
93 'url' => CRM_Utils_System::url('civicrm/admin/custom/group', 'reset=1'),
94 ),
95 'rel_table_uf_groups' => array(
96 'title' => ts('Profiles'),
97 'tables' => array('civicrm_uf_group'),
98 'url' => CRM_Utils_System::url('civicrm/admin/uf/group', 'reset=1'),
99 ),
100 'rel_table_groups' => array(
101 'title' => ts('Groups'),
102 'tables' => array('civicrm_group_contact'),
103 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=group'),
104 ),
105 'rel_table_notes' => array(
106 'title' => ts('Notes'),
107 'tables' => array('civicrm_note'),
108 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=note'),
109 ),
110 'rel_table_tags' => array(
111 'title' => ts('Tags'),
112 'tables' => array('civicrm_entity_tag'),
113 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=tag'),
114 ),
115 'rel_table_mailings' => array(
116 'title' => ts('Mailings'),
117 'tables' => array('civicrm_mailing', 'civicrm_mailing_event_queue', 'civicrm_mailing_event_subscribe'),
118 'url' => CRM_Utils_System::url('civicrm/mailing', 'reset=1&force=1&cid=$cid'),
119 ),
120 'rel_table_cases' => array(
121 'title' => ts('Cases'),
122 'tables' => array('civicrm_case_contact'),
123 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=case'),
124 ),
125 'rel_table_grants' => array(
126 'title' => ts('Grants'),
127 'tables' => array('civicrm_grant'),
128 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=grant'),
129 ),
130 'rel_table_pcp' => array(
131 'title' => ts('PCPs'),
132 'tables' => array('civicrm_pcp'),
133 'url' => CRM_Utils_System::url('civicrm/contribute/pcp/manage', 'reset=1'),
134 ),
135 'rel_table_pledges' => array(
136 'title' => ts('Pledges'),
137 'tables' => array('civicrm_pledge', 'civicrm_pledge_payment'),
138 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid&selectedChild=pledge'),
139 ),
140 'rel_table_users' => array(
141 'title' => $title,
142 'tables' => array('civicrm_uf_match'),
143 'url' => $userRecordUrl,
144 ),
145 );
146
147 $relTables += self::getMultiValueCustomSets('relTables');
148
149 // Allow hook_civicrm_merge() to adjust $relTables
150 CRM_Utils_Hook::merge('relTables', $relTables);
151 }
152 return $relTables;
153 }
154
155 /**
156 * Returns the related tables groups for which a contact has any info entered
157 */
158 static function getActiveRelTables($cid) {
159 $cid = (int) $cid;
160 $groups = array();
161
162 $relTables = self::relTables();
163 $cidRefs = self::cidRefs();
164 $eidRefs = self::eidRefs();
165 foreach ($relTables as $group => $params) {
166 $sqls = array();
167 foreach ($params['tables'] as $table) {
168 if (isset($cidRefs[$table])) {
169 foreach ($cidRefs[$table] as $field) {
170 $sqls[] = "SELECT COUNT(*) AS count FROM $table WHERE $field = $cid";
171 }
172 }
173 if (isset($eidRefs[$table])) {
174 foreach ($eidRefs[$table] as $entityTable => $entityId) {
175 $sqls[] = "SELECT COUNT(*) AS count FROM $table WHERE $entityId = $cid AND $entityTable = 'civicrm_contact'";
176 }
177 }
178 foreach ($sqls as $sql) {
179 if (CRM_Core_DAO::singleValueQuery($sql) > 0) {
180 $groups[] = $group;
181 }
182 }
183 }
184 }
185 return array_unique($groups);
186 }
187
188 /**
189 * Return tables and their fields referencing civicrm_contact.contact_id explicitly
190 */
191 static function cidRefs() {
192 static $cidRefs;
193 if (!$cidRefs) {
194 $sql = "
195 SELECT
196 table_name,
197 column_name
198 FROM information_schema.key_column_usage
199 WHERE
200 referenced_table_schema = database() AND
201 referenced_table_name = 'civicrm_contact' AND
202 referenced_column_name = 'id';
203 ";
204 $dao = CRM_Core_DAO::executeQuery($sql);
205 while ($dao->fetch()) {
206 $cidRefs[$dao->table_name][] = $dao->column_name;
207 }
208
209 // FixME for time being adding below line statically as no Foreign key constraint defined for table 'civicrm_entity_tag'
210 $cidRefs['civicrm_entity_tag'][] = 'entity_id';
211 $dao->free();
212
213 // Allow hook_civicrm_merge() to adjust $cidRefs
214 CRM_Utils_Hook::merge('cidRefs', $cidRefs);
215 }
216 return $cidRefs;
217 }
218
219 /**
220 * Return tables and their fields referencing civicrm_contact.contact_id with entity_id
221 */
222 static function eidRefs() {
223 static $eidRefs;
224 if (!$eidRefs) {
225 // FIXME: this should be generated dynamically from the schema
226 // tables that reference contacts with entity_{id,table}
227 $eidRefs = array(
228 'civicrm_acl' => array('entity_table' => 'entity_id'),
229 'civicrm_acl_entity_role' => array('entity_table' => 'entity_id'),
230 'civicrm_entity_file' => array('entity_table' => 'entity_id'),
231 'civicrm_log' => array('entity_table' => 'entity_id'),
232 'civicrm_mailing_group' => array('entity_table' => 'entity_id'),
233 'civicrm_note' => array('entity_table' => 'entity_id'),
234 );
235
236 // Allow hook_civicrm_merge() to adjust $eidRefs
237 CRM_Utils_Hook::merge('eidRefs', $eidRefs);
238 }
239 return $eidRefs;
240 }
241
242 /**
243 * We treat multi-valued custom sets as "related tables" similar to activities, contributions, etc.
244 * @param string $request 'relTables' or 'cidRefs'
245 * @see CRM-13836
246 */
247 static function getMultiValueCustomSets($request) {
248 static $data = NULL;
249 if ($data === NULL) {
250 $data = array(
251 'relTables' => array(),
252 'cidRefs' => array(),
253 );
254 $result = civicrm_api3('custom_group', 'get', array(
255 'is_multiple' => 1,
256 'extends' => array('IN' => array('Individual', 'Organization', 'Household', 'Contact')),
257 'return' => array('id', 'title', 'table_name', 'style'),
258 ));
259 foreach($result['values'] as $custom) {
260 $data['cidRefs'][$custom['table_name']] = array('entity_id');
261 $urlSuffix = $custom['style'] == 'Tab' ? '&selectedChild=custom_' . $custom['id'] : '';
262 $data['relTables']['rel_table_custom_' . $custom['id']] = array(
263 'title' => $custom['title'],
264 'tables' => array($custom['table_name']),
265 'url' => CRM_Utils_System::url('civicrm/contact/view', 'reset=1&force=1&cid=$cid' . $urlSuffix),
266 );
267 }
268 }
269 return $data[$request];
270 }
271
272 /**
273 * Tables which require custom processing should declare functions to call here.
274 * Doing so will override normal processing.
275 */
276 static function cpTables() {
277 static $tables;
278 if (!$tables) {
279 $tables = array(
280 'civicrm_case_contact' => array('CRM_Case_BAO_Case' => 'mergeContacts'),
281 'civicrm_group_contact' => array('CRM_Contact_BAO_GroupContact' => 'mergeGroupContact'),
282 // Empty array == do nothing - this table is handled by mergeGroupContact
283 'civicrm_subscription_history' => array(),
284 'civicrm_relationship' => array('CRM_Contact_BAO_Relationship' => 'mergeRelationships'),
285 );
286 }
287 return $tables;
288 }
289
290 /**
291 * return payment related table.
292 */
293 static function paymentTables() {
294 static $tables;
295 if (!$tables) {
296 $tables = array('civicrm_pledge', 'civicrm_membership', 'civicrm_participant');
297 }
298
299 return $tables;
300 }
301
302 /**
303 * return payment update Query.
304 */
305 static function paymentSql($tableName, $mainContactId, $otherContactId) {
306 $sqls = array();
307 if (!$tableName || !$mainContactId || !$otherContactId) {
308 return $sqls;
309 }
310
311 $paymentTables = self::paymentTables();
312 if (!in_array($tableName, $paymentTables)) {
313 return $sqls;
314 }
315
316 switch ($tableName) {
317 case 'civicrm_pledge':
318 $sqls[] = "
319 UPDATE IGNORE civicrm_contribution contribution
320 INNER JOIN civicrm_pledge_payment payment ON ( payment.contribution_id = contribution.id )
321 INNER JOIN civicrm_pledge pledge ON ( pledge.id = payment.pledge_id )
322 SET contribution.contact_id = $mainContactId
323 WHERE pledge.contact_id = $otherContactId";
324 break;
325
326 case 'civicrm_membership':
327 $sqls[] = "
328 UPDATE IGNORE civicrm_contribution contribution
329 INNER JOIN civicrm_membership_payment payment ON ( payment.contribution_id = contribution.id )
330 INNER JOIN civicrm_membership membership ON ( membership.id = payment.membership_id )
331 SET contribution.contact_id = $mainContactId
332 WHERE membership.contact_id = $otherContactId";
333 break;
334
335 case 'civicrm_participant':
336 $sqls[] = "
337 UPDATE IGNORE civicrm_contribution contribution
338 INNER JOIN civicrm_participant_payment payment ON ( payment.contribution_id = contribution.id )
339 INNER JOIN civicrm_participant participant ON ( participant.id = payment.participant_id )
340 SET contribution.contact_id = $mainContactId
341 WHERE participant.contact_id = $otherContactId";
342 break;
343 }
344
345 return $sqls;
346 }
347
348 static function operationSql($mainId, $otherId, $tableName, $tableOperations = array(), $mode = 'add') {
349 $sqls = array();
350 if (!$tableName || !$mainId || !$otherId) {
351 return $sqls;
352 }
353
354
355 switch ($tableName) {
356 case 'civicrm_membership':
357 if (array_key_exists($tableName, $tableOperations) && $tableOperations[$tableName]['add'])
358 break;
359 if ($mode == 'add') {
360 $sqls[] = "
361 DELETE membership1.* FROM civicrm_membership membership1
362 INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = membership2.membership_type_id
363 AND membership1.contact_id = {$mainId}
364 AND membership2.contact_id = {$otherId} ";
365 }
366 if ($mode == 'payment') {
367 $sqls[] = "
368 DELETE contribution.* FROM civicrm_contribution contribution
369 INNER JOIN civicrm_membership_payment payment ON payment.contribution_id = contribution.id
370 INNER JOIN civicrm_membership membership1 ON membership1.id = payment.membership_id
371 AND membership1.contact_id = {$mainId}
372 INNER JOIN civicrm_membership membership2 ON membership1.membership_type_id = membership2.membership_type_id
373 AND membership2.contact_id = {$otherId}";
374 }
375 break;
376
377 case 'civicrm_uf_match':
378 // normal queries won't work for uf_match since that will lead to violation of unique constraint,
379 // failing to meet intended result. Therefore we introduce this additonal query:
380 $sqls[] = "DELETE FROM civicrm_uf_match WHERE contact_id = {$mainId}";
381 break;
382 }
383
384 return $sqls;
385 }
386
387 /**
388 * Based on the provided two contact_ids and a set of tables, move the
389 * belongings of the other contact to the main one.
390 *
391 * @static
392 */
393 static function moveContactBelongings($mainId, $otherId, $tables = FALSE, $tableOperations = array()) {
394 $cidRefs = self::cidRefs();
395 $eidRefs = self::eidRefs();
396 $cpTables = self::cpTables();
397 $paymentTables = self::paymentTables();
398 $membershipMerge = FALSE; // CRM-12695
399
400 $affected = array_merge(array_keys($cidRefs), array_keys($eidRefs));
401 if ($tables !== FALSE) {
402 // if there are specific tables, sanitize the list
403 $affected = array_unique(array_intersect($affected, $tables));
404 }
405 else {
406 // if there aren't any specific tables, don't affect the ones handled by relTables()
407 $relTables = self::relTables();
408 $handled = array();
409 foreach ($relTables as $params) {
410 $handled = array_merge($handled, $params['tables']);
411 }
412 $affected = array_diff($affected, $handled);
413 /**
414 * CRM-12695
415 * Set $membershipMerge flag only once
416 * while doing contact related migration
417 * to call addMembershipToRealtedContacts()
418 * function only once.
419 * Since the current function (moveContactBelongings) is called twice
420 * with & without parameters $tables & $tableOperations
421 */
422 // retrieve main contact's related table(s)
423 $activeMainRelTables = CRM_Dedupe_Merger::getActiveRelTables($mainId);
424 // check if membership table exists in main contact's related table(s)
425 if (in_array('rel_table_memberships', $activeMainRelTables)) {
426 $membershipMerge = TRUE; // set membership flag - CRM-12695
427 }
428 }
429
430 $mainId = (int) $mainId;
431 $otherId = (int) $otherId;
432
433 $sqls = array();
434 foreach ($affected as $table) {
435 // Call custom processing function for objects that require it
436 if (isset($cpTables[$table])) {
437 foreach ($cpTables[$table] as $className => $fnName) {
438 $className::$fnName($mainId, $otherId, $sqls);
439 }
440 // Skip normal processing
441 continue;
442 }
443
444 // use UPDATE IGNORE + DELETE query pair to skip on situations when
445 // there's a UNIQUE restriction on ($field, some_other_field) pair
446 if (isset($cidRefs[$table])) {
447 foreach ($cidRefs[$table] as $field) {
448 // carry related contributions CRM-5359
449 if (in_array($table, $paymentTables)) {
450 $payOprSqls = self::operationSql($mainId, $otherId, $table, $tableOperations, 'payment');
451 $sqls = array_merge($sqls, $payOprSqls);
452
453 $paymentSqls = self::paymentSql($table, $mainId, $otherId);
454 $sqls = array_merge($sqls, $paymentSqls);
455 }
456
457 $preOperationSqls = self::operationSql($mainId, $otherId, $table, $tableOperations);
458 $sqls = array_merge($sqls, $preOperationSqls);
459
460 $sqls[] = "UPDATE IGNORE $table SET $field = $mainId WHERE $field = $otherId";
461 $sqls[] = "DELETE FROM $table WHERE $field = $otherId";
462 }
463 }
464 if (isset($eidRefs[$table])) {
465 foreach ($eidRefs[$table] as $entityTable => $entityId) {
466 $sqls[] = "UPDATE IGNORE $table SET $entityId = $mainId WHERE $entityId = $otherId AND $entityTable = 'civicrm_contact'";
467 $sqls[] = "DELETE FROM $table WHERE $entityId = $otherId AND $entityTable = 'civicrm_contact'";
468 }
469 }
470 }
471
472 // Allow hook_civicrm_merge() to add SQL statements for the merge operation.
473 CRM_Utils_Hook::merge('sqls', $sqls, $mainId, $otherId, $tables);
474
475 // call the SQL queries in one transaction
476 $transaction = new CRM_Core_Transaction();
477 foreach ($sqls as $sql) {
478 CRM_Core_DAO::executeQuery($sql, array(), TRUE, NULL, TRUE);
479 }
480 // CRM-12695
481 if ($membershipMerge) {
482 // call to function adding membership to related contacts
483 CRM_Dedupe_Merger::addMembershipToRealtedContacts($mainId);
484 }
485 $transaction->commit();
486 }
487
488 /**
489 * Find differences between contacts.
490 *
491 * @param array $main contact details
492 * @param array $other contact details
493 *
494 * @return array
495 * @static
496 */
497 static function findDifferences($main, $other) {
498 $result = array(
499 'contact' => array(),
500 'custom' => array(),
501 );
502 foreach (self::getContactFields() as $validField) {
503 if (CRM_Utils_Array::value($validField, $main) != CRM_Utils_Array::value($validField, $other)) {
504 $result['contact'][] = $validField;
505 }
506 }
507
508 $mainEvs = CRM_Core_BAO_CustomValueTable::getEntityValues($main['id']);
509 $otherEvs = CRM_Core_BAO_CustomValueTable::getEntityValues($other['id']);
510 $keys = array_unique(array_merge(array_keys($mainEvs), array_keys($otherEvs)));
511 foreach ($keys as $key) {
512 // Exclude multi-value fields CRM-13836
513 if (strpos($key, '_')) {
514 continue;
515 }
516 $key1 = CRM_Utils_Array::value($key, $mainEvs);
517 $key2 = CRM_Utils_Array::value($key, $otherEvs);
518 if ($key1 != $key2) {
519 $result['custom'][] = $key;
520 }
521 }
522 return $result;
523 }
524
525 /**
526 * Function to batch merge a set of contacts based on rule-group and group.
527 *
528 * @param int $rgid rule group id
529 * @param int $gid group id
530 * @param string $mode helps decide how to behave when there are conflicts.
531 * A 'safe' value skips the merge if there are any un-resolved conflicts.
532 * Does a force merge otherwise.
533 * @param boolean $autoFlip wether to let api decide which contact to retain and which to delete.
534 *
535 *
536 * @param bool $redirectForPerformance
537 *
538 * @return array|bool
539 * @internal param array $cacheParams prev-next-cache params based on which next pair of contacts are computed.
540 * Generally used with batch-merge.
541 * @static
542 * @access public
543 */
544 static function batchMerge($rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $redirectForPerformance = FALSE) {
545 $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
546 $cacheKeyString = "merge {$contactType}";
547 $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
548 $cacheKeyString .= $gid ? "_{$gid}" : '_0';
549 $join = "LEFT JOIN civicrm_dedupe_exception de ON ( pn.entity_id1 = de.contact_id1 AND
550 pn.entity_id2 = de.contact_id2 )";
551
552 $limit = $redirectForPerformance ? 75 : 1;
553 $where = "de.id IS NULL LIMIT {$limit}";
554
555 $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $where);
556 if (empty($dupePairs) && !$redirectForPerformance) {
557 // If we haven't found any dupes, probably cache is empty.
558 // Try filling cache and give another try.
559 CRM_Core_BAO_PrevNextCache::refillCache($rgid, $gid, $cacheKeyString);
560 $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $where);
561 }
562
563 $cacheParams = array(
564 'cache_key_string' => $cacheKeyString,
565 'join' => $join,
566 'where' => $where,
567 );
568 return CRM_Dedupe_Merger::merge($dupePairs, $cacheParams, $mode, $autoFlip, $redirectForPerformance);
569 }
570
571 /**
572 * Function to merge given set of contacts. Performs core operation.
573 *
574 * @param array $dupePairs set of pair of contacts for whom merge is to be done.
575 * @param array $cacheParams prev-next-cache params based on which next pair of contacts are computed.
576 * Generally used with batch-merge.
577 * @param string $mode helps decide how to behave when there are conflicts.
578 * A 'safe' value skips the merge if there are any un-resolved conflicts.
579 * Does a force merge otherwise (aggressive mode).
580 * @param boolean $autoFlip wether to let api decide which contact to retain and which to delete.
581 *
582 *
583 * @param bool $redirectForPerformance
584 *
585 * @return array|bool
586 * @static
587 * @access public
588 */
589 static function merge($dupePairs = array(
590 ), $cacheParams = array(), $mode = 'safe',
591 $autoFlip = TRUE, $redirectForPerformance = FALSE
592 ) {
593 $cacheKeyString = CRM_Utils_Array::value('cache_key_string', $cacheParams);
594 $resultStats = array('merged' => array(), 'skipped' => array());
595
596 // we don't want dupe caching to get reset after every-merge, and therefore set the
597 // doNotResetCache flag
598 $config = CRM_Core_Config::singleton();
599 $config->doNotResetCache = 1;
600
601 while (!empty($dupePairs)) {
602 foreach ($dupePairs as $dupes) {
603 CRM_Utils_Hook::merge('flip', $dupes, $dupes['dstID'], $dupes['srcID']);
604 $mainId = $dupes['dstID'];
605 $otherId = $dupes['srcID'];
606 $isAutoFlip = CRM_Utils_Array::value('auto_flip', $dupes, $autoFlip);
607 // if we can, make sure that $mainId is the one with lower id number
608 if ($isAutoFlip && ($mainId > $otherId)) {
609 $mainId = $dupes['srcID'];
610 $otherId = $dupes['dstID'];
611 }
612 if (!$mainId || !$otherId) {
613 // return error
614 return FALSE;
615 }
616
617 // Generate var $migrationInfo. The variable structure is exactly same as
618 // $formValues submitted during a UI merge for a pair of contacts.
619 $rowsElementsAndInfo = &CRM_Dedupe_Merger::getRowsElementsAndInfo($mainId, $otherId);
620
621 $migrationInfo = &$rowsElementsAndInfo['migration_info'];
622
623 // add additional details that we might need to resolve conflicts
624 $migrationInfo['main_details'] = &$rowsElementsAndInfo['main_details'];
625 $migrationInfo['other_details'] = &$rowsElementsAndInfo['other_details'];
626 $migrationInfo['main_loc_block'] = &$rowsElementsAndInfo['main_loc_block'];
627 $migrationInfo['rows'] = &$rowsElementsAndInfo['rows'];
628
629 // go ahead with merge if there is no conflict
630 if (!CRM_Dedupe_Merger::skipMerge($mainId, $otherId, $migrationInfo, $mode)) {
631 CRM_Dedupe_Merger::moveAllBelongings($mainId, $otherId, $migrationInfo);
632 $resultStats['merged'][] = array('main_d' => $mainId, 'other_id' => $otherId);
633 }
634 else {
635 $resultStats['skipped'][] = array('main_d' => $mainId, 'other_id' => $otherId);
636 }
637
638 // delete entry from PrevNextCache table so we don't consider the pair next time
639 // pair may have been flipped, so make sure we delete using both orders
640 CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString);
641 CRM_Core_BAO_PrevNextCache::deletePair($otherId, $mainId, $cacheKeyString);
642
643 CRM_Core_DAO::freeResult();
644 unset($rowsElementsAndInfo, $migrationInfo);
645 }
646
647 if ($cacheKeyString && !$redirectForPerformance) {
648 // retrieve next pair of dupes
649 $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString,
650 $cacheParams['join'],
651 $cacheParams['where']
652 );
653 }
654 else {
655 // do not proceed. Terminate the loop
656 unset($dupePairs);
657 }
658 }
659 return $resultStats;
660 }
661
662 /**
663 * A function which uses various rules / algorithms for choosing which contact to bias to
664 * when there's a conflict (to handle "gotchas"). Plus the safest route to merge.
665 *
666 * @param int $mainId main contact with whom merge has to happen
667 * @param int $otherId duplicate contact which would be deleted after merge operation
668 * @param array $migrationInfo array of information about which elements to merge.
669 * @param string $mode helps decide how to behave when there are conflicts.
670 * A 'safe' value skips the merge if there are any un-resolved conflicts.
671 * Does a force merge otherwise (aggressive mode).
672 *
673 * @return bool
674 * @static
675 * @access public
676 */
677 static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe') {
678 $conflicts = array();
679 $migrationData = array(
680 'old_migration_info' => $migrationInfo,
681 'mode' => $mode,
682 );
683 $allLocationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
684
685 foreach ($migrationInfo as $key => $val) {
686 if ($val === "null") {
687 // Rule: no overwriting with empty values in any mode
688 unset($migrationInfo[$key]);
689 continue;
690 }
691 elseif ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) or
692 substr($key, 0, 12) == 'move_custom_'
693 ) and $val != NULL) {
694 // Rule: if both main-contact has other-contact, let $mode decide if to merge a
695 // particular field or not
696 if (!empty($migrationInfo['rows'][$key]['main'])) {
697 // if main also has a value its a conflict
698 if ($mode == 'safe') {
699 // note it down & lets wait for response from the hook.
700 // For no response skip this merge
701 $conflicts[$key] = NULL;
702 }
703 elseif ($mode == 'aggressive') {
704 // let the main-field be overwritten
705 continue;
706 }
707 }
708 }
709 elseif (substr($key, 0, 14) == 'move_location_' and $val != NULL) {
710 $locField = explode('_', $key);
711 $fieldName = $locField[2];
712 $fieldCount = $locField[3];
713
714 // Rule: resolve address conflict if any -
715 if ($fieldName == 'address') {
716 $mainNewLocTypeId = $migrationInfo['location'][$fieldName][$fieldCount]['locTypeId'];
717 if (!empty($migrationInfo['main_loc_address']) &&
718 array_key_exists("main_{$mainNewLocTypeId}", $migrationInfo['main_loc_address'])) {
719 // main loc already has some address for the loc-type. Its a overwrite situation.
720
721 // look for next available loc-type
722 $newTypeId = NULL;
723 foreach ($allLocationTypes as $typeId => $typeLabel) {
724 if (!array_key_exists("main_{$typeId}", $migrationInfo['main_loc_address'])) {
725 $newTypeId = $typeId;
726 }
727 }
728 if ($newTypeId) {
729 // try insert address at new available loc-type
730 $migrationInfo['location'][$fieldName][$fieldCount]['locTypeId'] = $newTypeId;
731 }
732 elseif ($mode == 'safe') {
733 // note it down & lets wait for response from the hook.
734 // For no response skip this merge
735 $conflicts[$key] = NULL;
736 }
737 elseif ($mode == 'aggressive') {
738 // let the loc-type-id be same as that of other-contact & go ahead
739 // with merge assuming aggressive mode
740 continue;
741 }
742 }
743 }
744 elseif ($migrationInfo['rows'][$key]['main'] == $migrationInfo['rows'][$key]['other']) {
745 // for loc blocks other than address like email, phone .. if values are same no point in merging
746 // and adding redundant value
747 unset($migrationInfo[$key]);
748 }
749 }
750 }
751
752 // A hook to implement other algorithms for choosing which contact to bias to when
753 // there's a conflict (to handle "gotchas"). fields_in_conflict could be modified here
754 // merge happens with new values filled in here. For a particular field / row not to be merged
755 // field should be unset from fields_in_conflict.
756 $migrationData['fields_in_conflict'] = $conflicts;
757 CRM_Utils_Hook::merge('batch', $migrationData, $mainId, $otherId);
758 $conflicts = $migrationData['fields_in_conflict'];
759
760 if (!empty($conflicts)) {
761 foreach ($conflicts as $key => $val) {
762 if ($val === NULL and $mode == 'safe') {
763 // un-resolved conflicts still present. Lets skip this merge.
764 return TRUE;
765 }
766 else {
767 // copy over the resolved values
768 $migrationInfo[$key] = $val;
769 }
770 }
771 }
772 return FALSE;
773 }
774
775 /**
776 * A function to build an array of information required by merge function and the merge UI.
777 *
778 * @param int $mainId main contact with whom merge has to happen
779 * @param int $otherId duplicate contact which would be deleted after merge operation
780 *
781 * @return array|bool|int
782 * @static
783 * @access public
784 */
785 static function getRowsElementsAndInfo($mainId, $otherId) {
786 $qfZeroBug = 'e8cddb72-a257-11dc-b9cc-0016d3330ee9';
787
788 // Fetch contacts
789 foreach (array('main' => $mainId, 'other' => $otherId) as $moniker => $cid) {
790 $params = array('contact_id' => $cid, 'version' => 3, 'return' => array_merge(array('display_name'), self::getContactFields()));
791 $result = civicrm_api('contact', 'get', $params);
792
793 if (empty($result['values'][$cid]['contact_type'])) {
794 return FALSE;
795 }
796 $$moniker = $result['values'][$cid];
797 }
798
799 static $fields = array();
800 if (empty($fields)) {
801 $fields = CRM_Contact_DAO_Contact::fields();
802 CRM_Core_DAO::freeResult();
803 }
804
805 // FIXME: there must be a better way
806 foreach (array('main', 'other') as $moniker) {
807 $contact = &$$moniker;
808 $preferred_communication_method = CRM_Utils_array::value('preferred_communication_method', $contact);
809 $value = empty($preferred_communication_method) ? array() : $preferred_communication_method;
810 $specialValues[$moniker] = array(
811 'preferred_communication_method' => $value,
812 );
813
814 if (!empty($contact['preferred_communication_method'])){
815 // api 3 returns pref_comm_method as an array, which breaks the lookup; so we reconstruct
816 $prefCommList = is_array($specialValues[$moniker]['preferred_communication_method']) ?
817 implode(CRM_Core_DAO::VALUE_SEPARATOR, $specialValues[$moniker]['preferred_communication_method']) :
818 $specialValues[$moniker]['preferred_communication_method'];
819 $specialValues[$moniker]['preferred_communication_method'] = CRM_Core_DAO::VALUE_SEPARATOR . $prefCommList . CRM_Core_DAO::VALUE_SEPARATOR;
820 }
821 $names = array(
822 'preferred_communication_method' =>
823 array(
824 'newName' => 'preferred_communication_method_display',
825 'groupName' => 'preferred_communication_method',
826 ),
827 );
828 CRM_Core_OptionGroup::lookupValues($specialValues[$moniker], $names);
829 }
830
831 static $optionValueFields = array();
832 if (empty($optionValueFields)) {
833 $optionValueFields = CRM_Core_OptionValue::getFields();
834 }
835 foreach ($optionValueFields as $field => $params) {
836 $fields[$field]['title'] = $params['title'];
837 }
838
839 $diffs = self::findDifferences($main, $other);
840
841 $rows = $elements = $relTableElements = $migrationInfo = array();
842
843 foreach ($diffs['contact'] as $field) {
844 foreach (array('main', 'other') as $moniker) {
845 $contact = &$$moniker;
846 $value = CRM_Utils_Array::value($field, $contact);
847 if (isset($specialValues[$moniker][$field]) && is_string($specialValues[$moniker][$field])) {
848 $value = CRM_Core_DAO::VALUE_SEPARATOR . trim($specialValues[$moniker][$field], CRM_Core_DAO::VALUE_SEPARATOR) . CRM_Core_DAO::VALUE_SEPARATOR;
849 }
850 $label = isset($specialValues[$moniker]["{$field}_display"]) ? $specialValues[$moniker]["{$field}_display"] : $value;
851 if (!empty($fields[$field]['type']) && $fields[$field]['type'] == CRM_Utils_Type::T_DATE) {
852 if ($value) {
853 $value = str_replace('-', '', $value);
854 $label = CRM_Utils_Date::customFormat($label);
855 }
856 else {
857 $value = "null";
858 }
859 }
860 elseif (!empty($fields[$field]['type']) && $fields[$field]['type'] == CRM_Utils_Type::T_BOOLEAN) {
861 if ($label === '0') {
862 $label = ts('[ ]');
863 }
864 if ($label === '1') {
865 $label = ts('[x]');
866 }
867 }
868 elseif ($field == 'individual_prefix' || $field == 'prefix_id') {
869 $label = CRM_Utils_Array::value('individual_prefix', $contact);
870 $value = CRM_Utils_Array::value('prefix_id', $contact);
871 $field = 'prefix_id';
872 }
873 elseif ($field == 'individual_suffix' || $field == 'suffix_id') {
874 $label = CRM_Utils_Array::value('individual_suffix', $contact);
875 $value = CRM_Utils_Array::value('suffix_id', $contact);
876 $field = 'suffix_id';
877 }
878 $rows["move_$field"][$moniker] = $label;
879 if ($moniker == 'other') {
880 //CRM-14334
881 if ($value === NULL || $value == '') {
882 $value = 'null';
883 }
884 if ($value === 0 or $value === '0') {
885 $value = $qfZeroBug;
886 }
887 if (is_array($value) && empty($value[1])) {
888 $value[1] = NULL;
889 }
890 $elements[] = array('advcheckbox', "move_$field", NULL, NULL, NULL, $value);
891 $migrationInfo["move_$field"] = $value;
892 }
893 }
894 $rows["move_$field"]['title'] = $fields[$field]['title'];
895 }
896
897 // handle location blocks.
898 $locationBlocks = array('email', 'phone', 'address');
899 $locations = array();
900
901 foreach ($locationBlocks as $block) {
902 foreach (array('main' => $mainId, 'other' => $otherId) as $moniker => $cid) {
903 $cnt = 1;
904 $values = civicrm_api($block, 'get', array('contact_id' => $cid, 'version' => 3));
905 $count = $values['count'];
906 if ($count) {
907 if ($count > $cnt) {
908 foreach ($values['values'] as $value) {
909 if ($block == 'address') {
910 CRM_Core_BAO_Address::fixAddress($value);
911 $display = CRM_Utils_Address::format($value);
912 $locations[$moniker][$block][$cnt] = $value;
913 $locations[$moniker][$block][$cnt]['display'] = $display;
914 }
915 else {
916 $locations[$moniker][$block][$cnt] = $value;
917 }
918
919 $cnt++;
920 }
921 }
922 else {
923 $id = $values['id'];
924 if ($block == 'address') {
925 CRM_Core_BAO_Address::fixAddress($values['values'][$id]);
926 $display = CRM_Utils_Address::format($values['values'][$id]);
927 $locations[$moniker][$block][$cnt] = $values['values'][$id];
928 $locations[$moniker][$block][$cnt]['display'] = $display;
929 }
930 else {
931 $locations[$moniker][$block][$cnt] = $values['values'][$id];
932 }
933 }
934 }
935 }
936 }
937
938 $allLocationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
939
940 $mainLocBlock = $locBlockIds = array();
941 $locBlockIds['main'] = $locBlockIds['other'] = array();
942 foreach (array('Email', 'Phone', 'IM', 'OpenID', 'Address') as $block) {
943 $name = strtolower($block);
944 foreach (array('main', 'other') as $moniker) {
945 $locIndex = CRM_Utils_Array::value($moniker, $locations);
946 $blockValue = CRM_Utils_Array::value($name, $locIndex, array());
947 if (empty($blockValue)) {
948 $locValue[$moniker][$name] = 0;
949 $locLabel[$moniker][$name] = $locTypes[$moniker][$name] = array();
950 }
951 else {
952 $locValue[$moniker][$name] = TRUE;
953 foreach ($blockValue as $count => $blkValues) {
954 $fldName = $name;
955 $locTypeId = $blkValues['location_type_id'];
956 if ($name == 'im') {
957 $fldName = 'name';
958 }
959 if ($name == 'address') {
960 $fldName = 'display';
961 }
962 $locLabel[$moniker][$name][$count] = CRM_Utils_Array::value($fldName,
963 $blkValues
964 );
965 $locTypes[$moniker][$name][$count] = $locTypeId;
966 if ($moniker == 'main' && in_array($name, $locationBlocks)) {
967 $mainLocBlock["main_$name$locTypeId"] = CRM_Utils_Array::value($fldName,
968 $blkValues
969 );
970 $locBlockIds['main'][$name][$locTypeId] = $blkValues['id'];
971 }
972 else {
973 $locBlockIds[$moniker][$name][$count] = $blkValues['id'];
974 }
975 }
976 }
977 }
978
979 if ($locValue['other'][$name] != 0) {
980 foreach ($locLabel['other'][$name] as $count => $value) {
981 $locTypeId = $locTypes['other'][$name][$count];
982 $rows["move_location_{$name}_$count"]['other'] = $value;
983 $rows["move_location_{$name}_$count"]['main'] = CRM_Utils_Array::value($count,
984 $locLabel['main'][$name]
985 );
986 $rows["move_location_{$name}_$count"]['title'] = ts('%1:%2:%3',
987 array(
988 1 => $block,
989 2 => $count,
990 3 => $allLocationTypes[$locTypeId]
991 )
992 );
993
994 $elements[] = array('advcheckbox', "move_location_{$name}_{$count}");
995 $migrationInfo["move_location_{$name}_{$count}"] = 1;
996
997 // make sure default location type is always on top
998 $mainLocTypeId = CRM_Utils_Array::value($count, $locTypes['main'][$name], $locTypeId);
999 $locTypeValues = $allLocationTypes;
1000 $defaultLocType = array($mainLocTypeId => $locTypeValues[$mainLocTypeId]);
1001 unset($locTypeValues[$mainLocTypeId]);
1002
1003 // keep 1-1 mapping for address - location type.
1004 $js = NULL;
1005 if (in_array($name, $locationBlocks) && !empty($mainLocBlock)) {
1006 $js = array('onChange' => "mergeBlock('$name', this, $count );");
1007 }
1008 $elements[] = array(
1009 'select', "location[{$name}][$count][locTypeId]", NULL,
1010 $defaultLocType + $locTypeValues, $js,
1011 );
1012 // keep location-type-id same as that of other-contact
1013 $migrationInfo['location'][$name][$count]['locTypeId'] = $locTypeId;
1014
1015 if ($name != 'address') {
1016 $elements[] = array('advcheckbox', "location[{$name}][$count][operation]", NULL, ts('add new'));
1017 // always use add operation
1018 $migrationInfo['location'][$name][$count]['operation'] = 1;
1019 }
1020 }
1021 }
1022 }
1023
1024 // add the related tables and unset the ones that don't sport any of the duplicate contact's info
1025 $config = CRM_Core_Config::singleton();
1026 $mainUfId = CRM_Core_BAO_UFMatch::getUFId($mainId);
1027 $mainUser = NULL;
1028 if ($mainUfId) {
1029 // d6 compatible
1030 if ($config->userSystem->is_drupal == '1' && function_exists($mainUser)) {
1031 $mainUser = user_load($mainUfId);
1032 }
1033 elseif ($config->userFramework == 'Joomla') {
1034 $mainUser = JFactory::getUser($mainUfId);
1035 }
1036 }
1037 $otherUfId = CRM_Core_BAO_UFMatch::getUFId($otherId);
1038 $otherUser = NULL;
1039 if ($otherUfId) {
1040 // d6 compatible
1041 if ($config->userSystem->is_drupal == '1' && function_exists($mainUser)) {
1042 $otherUser = user_load($otherUfId);
1043 }
1044 elseif ($config->userFramework == 'Joomla') {
1045 $otherUser = JFactory::getUser($otherUfId);
1046 }
1047 }
1048
1049 $relTables = CRM_Dedupe_Merger::relTables();
1050 $activeRelTables = CRM_Dedupe_Merger::getActiveRelTables($otherId);
1051 $activeMainRelTables = CRM_Dedupe_Merger::getActiveRelTables($mainId);
1052 foreach ($relTables as $name => $null) {
1053 if (!in_array($name, $activeRelTables) &&
1054 !(($name == 'rel_table_users') && in_array($name, $activeMainRelTables))
1055 ) {
1056 unset($relTables[$name]);
1057 continue;
1058 }
1059
1060 $relTableElements[] = array('checkbox', "move_$name");
1061 $migrationInfo["move_$name"] = 1;
1062
1063 $relTables[$name]['main_url'] = str_replace('$cid', $mainId, $relTables[$name]['url']);
1064 $relTables[$name]['other_url'] = str_replace('$cid', $otherId, $relTables[$name]['url']);
1065 if ($name == 'rel_table_users') {
1066 $relTables[$name]['main_url'] = str_replace('%ufid', $mainUfId, $relTables[$name]['url']);
1067 $relTables[$name]['other_url'] = str_replace('%ufid', $otherUfId, $relTables[$name]['url']);
1068 $find = array('$ufid', '$ufname');
1069 if ($mainUser) {
1070 $replace = array($mainUfId, $mainUser->name);
1071 $relTables[$name]['main_title'] = str_replace($find, $replace, $relTables[$name]['title']);
1072 }
1073 if ($otherUser) {
1074 $replace = array($otherUfId, $otherUser->name);
1075 $relTables[$name]['other_title'] = str_replace($find, $replace, $relTables[$name]['title']);
1076 }
1077 }
1078 if ($name == 'rel_table_memberships') {
1079 $elements[] = array('checkbox', "operation[move_{$name}][add]", NULL, ts('add new'));
1080 $migrationInfo["operation"]["move_{$name}"]['add'] = 1;
1081 }
1082 }
1083 foreach ($relTables as $name => $null) {
1084 $relTables["move_$name"] = $relTables[$name];
1085 unset($relTables[$name]);
1086 }
1087
1088 // handle custom fields
1089 $mainTree = CRM_Core_BAO_CustomGroup::getTree($main['contact_type'], CRM_Core_DAO::$_nullObject, $mainId, -1,
1090 CRM_Utils_Array::value('contact_sub_type', $main)
1091 );
1092 $otherTree = CRM_Core_BAO_CustomGroup::getTree($main['contact_type'], CRM_Core_DAO::$_nullObject, $otherId, -1,
1093 CRM_Utils_Array::value('contact_sub_type', $other)
1094 );
1095 CRM_Core_DAO::freeResult();
1096
1097 foreach ($otherTree as $gid => $group) {
1098 $foundField = FALSE;
1099 if (!isset($group['fields'])) {
1100 continue;
1101 }
1102
1103 foreach ($group['fields'] as $fid => $field) {
1104 if (in_array($fid, $diffs['custom'])) {
1105 if (!$foundField) {
1106 $rows["custom_group_$gid"]['title'] = $group['title'];
1107 $foundField = TRUE;
1108 }
1109 if (!empty($mainTree[$gid]['fields'][$fid]['customValue'])) {
1110 foreach ($mainTree[$gid]['fields'][$fid]['customValue'] as $valueId => $values) {
1111 $rows["move_custom_$fid"]['main'] = CRM_Core_BAO_CustomGroup::formatCustomValues($values,
1112 $field, TRUE
1113 );
1114 }
1115 }
1116 $value = "null";
1117 if (!empty($otherTree[$gid]['fields'][$fid]['customValue'])) {
1118 foreach ($otherTree[$gid]['fields'][$fid]['customValue'] as $valueId => $values) {
1119 $rows["move_custom_$fid"]['other'] = CRM_Core_BAO_CustomGroup::formatCustomValues($values,
1120 $field, TRUE
1121 );
1122 if ($values['data'] === 0 || $values['data'] === '0') {
1123 $values['data'] = $qfZeroBug;
1124 }
1125 $value = ($values['data']) ? $values['data'] : $value;
1126 }
1127 }
1128 $rows["move_custom_$fid"]['title'] = $field['label'];
1129
1130 $elements[] = array('advcheckbox', "move_custom_$fid", NULL, NULL, NULL, $value);
1131 $migrationInfo["move_custom_$fid"] = $value;
1132 }
1133 }
1134 }
1135 $result = array(
1136 'rows' => $rows,
1137 'elements' => $elements,
1138 'rel_table_elements' => $relTableElements,
1139 'main_loc_block' => $mainLocBlock,
1140 'rel_tables' => $relTables,
1141 'main_details' => $main,
1142 'other_details' => $other,
1143 'migration_info' => $migrationInfo,
1144 );
1145
1146 $result['main_details']['loc_block_ids'] = $locBlockIds['main'];
1147 $result['other_details']['loc_block_ids'] = $locBlockIds['other'];
1148
1149 return $result;
1150 }
1151
1152 /**
1153 * Based on the provided two contact_ids and a set of tables, move the belongings of the
1154 * other contact to the main one - be it Location / CustomFields or Contact .. related info.
1155 * A superset of moveContactBelongings() function.
1156 *
1157 * @param int $mainId main contact with whom merge has to happen
1158 * @param int $otherId duplicate contact which would be deleted after merge operation
1159 *
1160 * @param $migrationInfo
1161 *
1162 * @return bool
1163 * @static
1164 * @access public
1165 */
1166 static function moveAllBelongings($mainId, $otherId, $migrationInfo) {
1167 if (empty($migrationInfo)) {
1168 return FALSE;
1169 }
1170
1171 $qfZeroBug = 'e8cddb72-a257-11dc-b9cc-0016d3330ee9';
1172 $relTables = CRM_Dedupe_Merger::relTables();
1173 $moveTables = $locBlocks = $tableOperations = array();
1174 foreach ($migrationInfo as $key => $value) {
1175 if ($value == $qfZeroBug) {
1176 $value = '0';
1177 }
1178 if ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) ||
1179 substr($key, 0, 12) == 'move_custom_') &&
1180 $value != NULL
1181 ) {
1182 $submitted[substr($key, 5)] = $value;
1183 }
1184 elseif (substr($key, 0, 14) == 'move_location_' and $value != NULL) {
1185 $locField = explode('_', $key);
1186 $fieldName = $locField[2];
1187 $fieldCount = $locField[3];
1188 $operation = CRM_Utils_Array::value('operation', $migrationInfo['location'][$fieldName][$fieldCount]);
1189 // default operation is overwrite.
1190 if (!$operation) {
1191 $operation = 2;
1192 }
1193
1194 $locBlocks[$fieldName][$fieldCount]['operation'] = $operation;
1195 $locBlocks[$fieldName][$fieldCount]['locTypeId'] = CRM_Utils_Array::value('locTypeId', $migrationInfo['location'][$fieldName][$fieldCount]);
1196 }
1197 elseif (substr($key, 0, 15) == 'move_rel_table_' and $value == '1') {
1198 $moveTables = array_merge($moveTables, $relTables[substr($key, 5)]['tables']);
1199 if (array_key_exists('operation', $migrationInfo)) {
1200 foreach ($relTables[substr($key, 5)]['tables'] as $table) {
1201 if (array_key_exists($key, $migrationInfo['operation'])) {
1202 $tableOperations[$table] = $migrationInfo['operation'][$key];
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209
1210 // **** Do location related migration:
1211 if (!empty($locBlocks)) {
1212 $locComponent = array(
1213 'email' => 'Email',
1214 'phone' => 'Phone',
1215 'im' => 'IM',
1216 'openid' => 'OpenID',
1217 'address' => 'Address',
1218 );
1219
1220 $primaryBlockIds = CRM_Contact_BAO_Contact::getLocBlockIds($mainId, array('is_primary' => 1));
1221 $billingBlockIds = CRM_Contact_BAO_Contact::getLocBlockIds($mainId, array('is_billing' => 1));
1222
1223 foreach ($locBlocks as $name => $block) {
1224 if (!is_array($block) || CRM_Utils_System::isNull($block)) {
1225 continue;
1226 }
1227 $daoName = 'CRM_Core_DAO_' . $locComponent[$name];
1228 $primaryDAOId = (array_key_exists($name, $primaryBlockIds)) ? array_pop($primaryBlockIds[$name]) : NULL;
1229 $billingDAOId = (array_key_exists($name, $billingBlockIds)) ? array_pop($billingBlockIds[$name]) : NULL;
1230
1231 foreach ($block as $blkCount => $values) {
1232 $locTypeId = CRM_Utils_Array::value('locTypeId', $values, 1);
1233 $operation = CRM_Utils_Array::value('operation', $values, 2);
1234 $otherBlockId = CRM_Utils_Array::value($blkCount,
1235 $migrationInfo['other_details']['loc_block_ids'][$name]
1236 );
1237
1238 // keep 1-1 mapping for address - loc type.
1239 $idKey = $blkCount;
1240 if (array_key_exists($name, $locComponent)) {
1241 $idKey = $locTypeId;
1242 }
1243
1244 if (isset($migrationInfo['main_details']['loc_block_ids'][$name])) {
1245 $mainBlockId = CRM_Utils_Array::value($idKey, $migrationInfo['main_details']['loc_block_ids'][$name]);
1246 }
1247
1248 if (!$otherBlockId) {
1249 continue;
1250 }
1251
1252 // for the block which belongs to other-contact, link the contact to main-contact
1253 $otherBlockDAO = new $daoName();
1254 $otherBlockDAO->id = $otherBlockId;
1255 $otherBlockDAO->contact_id = $mainId;
1256 $otherBlockDAO->location_type_id = $locTypeId;
1257
1258 // if main contact already has primary & billing, set the flags to 0.
1259 if ($primaryDAOId) {
1260 $otherBlockDAO->is_primary = 0;
1261 }
1262 if ($billingDAOId) {
1263 $otherBlockDAO->is_billing = 0;
1264 }
1265
1266 // overwrite - need to delete block which belongs to main-contact.
1267 if ($mainBlockId && ($operation == 2)) {
1268 $deleteDAO = new $daoName();
1269 $deleteDAO->id = $mainBlockId;
1270 $deleteDAO->find(TRUE);
1271
1272 // if we about to delete a primary / billing block, set the flags for new block
1273 // that we going to assign to main-contact
1274 if ($primaryDAOId && ($primaryDAOId == $deleteDAO->id)) {
1275 $otherBlockDAO->is_primary = 1;
1276 }
1277 if ($billingDAOId && ($billingDAOId == $deleteDAO->id)) {
1278 $otherBlockDAO->is_billing = 1;
1279 }
1280
1281 $deleteDAO->delete();
1282 $deleteDAO->free();
1283 }
1284
1285 $otherBlockDAO->update();
1286 $otherBlockDAO->free();
1287 }
1288 }
1289 }
1290
1291 // **** Do tables related migrations
1292 if (!empty($moveTables)) {
1293 CRM_Dedupe_Merger::moveContactBelongings($mainId, $otherId, $moveTables, $tableOperations);
1294 unset($moveTables, $tableOperations);
1295 }
1296
1297 // **** Do contact related migrations
1298 CRM_Dedupe_Merger::moveContactBelongings($mainId, $otherId);
1299
1300 // FIXME: fix gender, prefix and postfix, so they're edible by createProfileContact()
1301 $names['gender'] = array('newName' => 'gender_id', 'groupName' => 'gender');
1302 $names['individual_prefix'] = array('newName' => 'prefix_id', 'groupName' => 'individual_prefix');
1303 $names['individual_suffix'] = array('newName' => 'suffix_id', 'groupName' => 'individual_suffix');
1304 $names['communication_style'] = array('newName' => 'communication_style_id', 'groupName' => 'communication_style');
1305 $names['addressee'] = array('newName' => 'addressee_id', 'groupName' => 'addressee');
1306 $names['email_greeting'] = array('newName' => 'email_greeting_id', 'groupName' => 'email_greeting');
1307 $names['postal_greeting'] = array('newName' => 'postal_greeting_id', 'groupName' => 'postal_greeting');
1308 CRM_Core_OptionGroup::lookupValues($submitted, $names, TRUE);
1309
1310 // fix custom fields so they're edible by createProfileContact()
1311 static $treeCache = array();
1312 if (!array_key_exists($migrationInfo['main_details']['contact_type'], $treeCache)) {
1313 $treeCache[$migrationInfo['main_details']['contact_type']] = CRM_Core_BAO_CustomGroup::getTree($migrationInfo['main_details']['contact_type'],
1314 CRM_Core_DAO::$_nullObject, NULL, -1
1315 );
1316 }
1317 $cgTree = &$treeCache[$migrationInfo['main_details']['contact_type']];
1318
1319 $cFields = array();
1320 foreach ($cgTree as $key => $group) {
1321 if (!isset($group['fields'])) {
1322 continue;
1323 }
1324 foreach ($group['fields'] as $fid => $field) {
1325 $cFields[$fid]['attributes'] = $field;
1326 }
1327 }
1328
1329 if (!isset($submitted)) {
1330 $submitted = array();
1331 }
1332 foreach ($submitted as $key => $value) {
1333 if (substr($key, 0, 7) == 'custom_') {
1334 $fid = (int) substr($key, 7);
1335 $htmlType = $cFields[$fid]['attributes']['html_type'];
1336 switch ($htmlType) {
1337 case 'File':
1338 $customFiles[] = $fid;
1339 unset($submitted["custom_$fid"]);
1340 break;
1341
1342 case 'Select Country':
1343 case 'Select State/Province':
1344 $submitted[$key] = CRM_Core_BAO_CustomField::getDisplayValue($value, $fid, $cFields);
1345 break;
1346
1347 case 'CheckBox':
1348 case 'AdvMulti-Select':
1349 case 'Multi-Select':
1350 case 'Multi-Select Country':
1351 case 'Multi-Select State/Province':
1352 // Merge values from both contacts for multivalue fields, CRM-4385
1353 // get the existing custom values from db.
1354 $customParams = array('entityID' => $mainId, $key => TRUE);
1355 $customfieldValues = CRM_Core_BAO_CustomValueTable::getValues($customParams);
1356 if (!empty($customfieldValues[$key])) {
1357 $existingValue = explode(CRM_Core_DAO::VALUE_SEPARATOR, $customfieldValues[$key]);
1358 if (is_array($existingValue) && !empty($existingValue)) {
1359 $mergeValue = $submmtedCustomValue = array();
1360 if ($value) {
1361 $submmtedCustomValue = explode(CRM_Core_DAO::VALUE_SEPARATOR, $value);
1362 }
1363
1364 //hack to remove null and duplicate values from array.
1365 foreach (array_merge($submmtedCustomValue, $existingValue) as $k => $v) {
1366 if ($v != '' && !in_array($v, $mergeValue)) {
1367 $mergeValue[] = $v;
1368 }
1369 }
1370
1371 //keep state and country as array format.
1372 //for checkbox and m-select format w/ VALUE_SEPARATOR
1373 if (in_array($htmlType, array(
1374 'CheckBox', 'Multi-Select', 'AdvMulti-Select'))) {
1375 $submitted[$key] = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR,
1376 $mergeValue
1377 ) . CRM_Core_DAO::VALUE_SEPARATOR;
1378 }
1379 else {
1380 $submitted[$key] = $mergeValue;
1381 }
1382 }
1383 }
1384 elseif (in_array($htmlType, array(
1385 'Multi-Select Country', 'Multi-Select State/Province'))) {
1386 //we require submitted values should be in array format
1387 if ($value) {
1388 $mergeValueArray = explode(CRM_Core_DAO::VALUE_SEPARATOR, $value);
1389 //hack to remove null values from array.
1390 $mergeValue = array();
1391 foreach ($mergeValueArray as $k => $v) {
1392 if ($v != '') {
1393 $mergeValue[] = $v;
1394 }
1395 }
1396 $submitted[$key] = $mergeValue;
1397 }
1398 }
1399 break;
1400
1401 default:
1402 break;
1403 }
1404 }
1405 }
1406
1407 // **** Do file custom fields related migrations
1408 // FIXME: move this someplace else (one of the BAOs) after discussing
1409 // where to, and whether CRM_Core_BAO_File::deleteFileReferences() shouldn't actually,
1410 // like, delete a file...
1411
1412 if (!isset($customFiles)) {
1413 $customFiles = array();
1414 }
1415 foreach ($customFiles as $customId) {
1416 list($tableName, $columnName, $groupID) = CRM_Core_BAO_CustomField::getTableColumnGroup($customId);
1417
1418 // get the contact_id -> file_id mapping
1419 $fileIds = array();
1420 $sql = "SELECT entity_id, {$columnName} AS file_id FROM {$tableName} WHERE entity_id IN ({$mainId}, {$otherId})";
1421 $dao = CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray);
1422 while ($dao->fetch()) {
1423 $fileIds[$dao->entity_id] = $dao->file_id;
1424 }
1425 $dao->free();
1426
1427 // delete the main contact's file
1428 if (!empty($fileIds[$mainId])) {
1429 CRM_Core_BAO_File::deleteFileReferences($fileIds[$mainId], $mainId, $customId);
1430 }
1431
1432 // move the other contact's file to main contact
1433 //NYSS need to INSERT or UPDATE depending on whether main contact has an existing record
1434 if ( CRM_Core_DAO::singleValueQuery("SELECT id FROM {$tableName} WHERE entity_id = {$mainId}") ) {
1435 $sql = "UPDATE {$tableName} SET {$columnName} = {$fileIds[$otherId]} WHERE entity_id = {$mainId}";
1436 }
1437 else {
1438 $sql = "INSERT INTO {$tableName} ( entity_id, {$columnName} ) VALUES ( {$mainId}, {$fileIds[$otherId]} )";
1439 }
1440 CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray);
1441
1442 if ( CRM_Core_DAO::singleValueQuery("
1443 SELECT id
1444 FROM civicrm_entity_file
1445 WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}") ) {
1446 $sql = "
1447 UPDATE civicrm_entity_file
1448 SET entity_id = {$mainId}
1449 WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}";
1450 }
1451 else {
1452 $sql = "
1453 INSERT INTO civicrm_entity_file ( entity_table, entity_id, file_id )
1454 VALUES ( '{$tableName}', {$mainId}, {$fileIds[$otherId]} )";
1455 }
1456 CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray);
1457 }
1458
1459 // move view only custom fields CRM-5362
1460 $viewOnlyCustomFields = array();
1461 foreach ($submitted as $key => $value) {
1462 $fid = (int) substr($key, 7);
1463 if (array_key_exists($fid, $cFields) && !empty($cFields[$fid]['attributes']['is_view'])) {
1464 $viewOnlyCustomFields[$key] = $value;
1465 }
1466 }
1467
1468 // special case to set values for view only, CRM-5362
1469 if (!empty($viewOnlyCustomFields)) {
1470 $viewOnlyCustomFields['entityID'] = $mainId;
1471 CRM_Core_BAO_CustomValueTable::setValues($viewOnlyCustomFields);
1472 }
1473
1474 // **** Delete other contact & update prev-next caching
1475 $otherParams = array(
1476 'contact_id' => $otherId,
1477 'id' => $otherId,
1478 'version' => 3,
1479 );
1480 if (CRM_Core_Permission::check('merge duplicate contacts') &&
1481 CRM_Core_Permission::check('delete contacts')
1482 ) {
1483 // if ext id is submitted then set it null for contact to be deleted
1484 if (!empty($submitted['external_identifier'])) {
1485 $query = "UPDATE civicrm_contact SET external_identifier = null WHERE id = {$otherId}";
1486 CRM_Core_DAO::executeQuery($query);
1487 }
1488
1489 civicrm_api('contact', 'delete', $otherParams);
1490 CRM_Core_BAO_PrevNextCache::deleteItem($otherId);
1491 }
1492 // FIXME: else part
1493 /* else { */
1494
1495 /* CRM_Core_Session::setStatus( ts('Do not have sufficient permission to delete duplicate contact.') ); */
1496
1497 /* } */
1498
1499
1500 // **** Update contact related info for the main contact
1501 if (!empty($submitted)) {
1502 $submitted['contact_id'] = $mainId;
1503
1504 //update current employer field
1505 if ($currentEmloyerId = CRM_Utils_Array::value('current_employer_id', $submitted)) {
1506 if (!CRM_Utils_System::isNull($currentEmloyerId)) {
1507 $submitted['current_employer'] = $submitted['current_employer_id'];
1508 } else {
1509 $submitted['current_employer'] = '';
1510 }
1511 unset($submitted['current_employer_id']);
1512 }
1513
1514 //CRM-14312 include prefix/suffix from mainId if not overridden for proper construction of display/sort name
1515 if ( !isset($submitted['prefix_id']) && !empty($migrationInfo['main_details']['prefix_id']) ) {
1516 $submitted['prefix_id'] = $migrationInfo['main_details']['prefix_id'];
1517 }
1518 if ( !isset($submitted['suffix_id']) && !empty($migrationInfo['main_details']['suffix_id']) ) {
1519 $submitted['suffix_id'] = $migrationInfo['main_details']['suffix_id'];
1520 }
1521
1522 CRM_Contact_BAO_Contact::createProfileContact($submitted, CRM_Core_DAO::$_nullArray, $mainId);
1523 unset($submitted);
1524 }
1525
1526 return TRUE;
1527 }
1528
1529 /**
1530 * @return array of field names which will be compared, so everything except ID.
1531 */
1532 static function getContactFields() {
1533 $contactFields = CRM_Contact_DAO_Contact::fields();
1534 $invalidFields = array('api_key', 'contact_is_deleted', 'created_date', 'display_name', 'hash', 'id', 'modified_date',
1535 'primary_contact_id', 'sort_name', 'user_unique_id');
1536 foreach ($contactFields as $field => $value) {
1537 if (in_array($field, $invalidFields)) {
1538 unset($contactFields[$field]);
1539 }
1540 }
1541 return array_keys($contactFields);
1542 }
1543
1544 /**
1545 * Added for CRM-12695
1546 * Based on the contactId provided
1547 * add/update membership(s) to related contacts
1548 *
1549 * @param contactId
1550 */
1551 static function addMembershipToRealtedContacts($contactID) {
1552 $dao = new CRM_Member_DAO_Membership();
1553 $dao->contact_id = $contactID;
1554 $dao->is_test = 0;
1555 $dao->find();
1556
1557 //checks membership of contact itself
1558 while ($dao->fetch()) {
1559 $relationshipTypeId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $dao->membership_type_id, 'relationship_type_id', 'id');
1560 if ($relationshipTypeId) {
1561 $membershipParams = array(
1562 'id' => $dao->id,
1563 'contact_id' => $dao->contact_id,
1564 'membership_type_id' => $dao->membership_type_id,
1565 'join_date' => CRM_Utils_Date::isoToMysql($dao->join_date),
1566 'start_date' => CRM_Utils_Date::isoToMysql($dao->start_date),
1567 'end_date' => CRM_Utils_Date::isoToMysql($dao->end_date),
1568 'source' => $dao->source,
1569 'status_id' => $dao->status_id
1570 );
1571 // create/update membership(s) for related contact(s)
1572 CRM_Member_BAO_Membership::createRelatedMemberships($membershipParams, $dao);
1573 } // end of if relationshipTypeId
1574 }
1575 }
1576 }
1577