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