/** * A function which uses various rules / algorithms for choosing which contact to bias to * when there's a conflict (to handle "gotchas"). Plus the safest route to merge. * * @param int $mainId * Main contact with whom merge has to happen. * @param int $otherId * Duplicate contact which would be deleted after merge operation. * @param array $migrationInfo * Array of information about which elements to merge. * @param string $mode * Helps decide how to behave when there are conflicts. * A 'safe' value skips the merge if there are any un-resolved conflicts. * Does a force merge otherwise (aggressive mode). * * @return bool */ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe') { $conflicts = array(); $migrationData = array('old_migration_info' => $migrationInfo, 'mode' => $mode); $allLocationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id'); foreach ($migrationInfo as $key => $val) { if ($val === "null") { // Rule: no overwriting with empty values in any mode unset($migrationInfo[$key]); continue; } elseif ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) or substr($key, 0, 12) == 'move_custom_') and $val != NULL) { // Rule: if both main-contact has other-contact, let $mode decide if to merge a // particular field or not if (!empty($migrationInfo['rows'][$key]['main'])) { // if main also has a value its a conflict if ($mode == 'safe') { // note it down & lets wait for response from the hook. // For no response skip this merge $conflicts[$key] = NULL; } elseif ($mode == 'aggressive') { // let the main-field be overwritten continue; } } } elseif (substr($key, 0, 14) == 'move_location_' and $val != NULL) { $locField = explode('_', $key); $fieldName = $locField[2]; $fieldCount = $locField[3]; // Rule: resolve address conflict if any - if ($fieldName == 'address') { $mainNewLocTypeId = $migrationInfo['location'][$fieldName][$fieldCount]['locTypeId']; if (!empty($migrationInfo['main_loc_block']) && array_key_exists("main_address{$mainNewLocTypeId}", $migrationInfo['main_loc_block'])) { // main loc already has some address for the loc-type. Its a overwrite situation. // look for next available loc-type $newTypeId = NULL; foreach ($allLocationTypes as $typeId => $typeLabel) { if (!array_key_exists("main_address{$typeId}", $migrationInfo['main_loc_block'])) { $newTypeId = $typeId; } } if ($newTypeId) { // try insert address at new available loc-type $migrationInfo['location'][$fieldName][$fieldCount]['locTypeId'] = $newTypeId; } elseif ($mode == 'safe') { // note it down & lets wait for response from the hook. // For no response skip this merge $conflicts[$key] = NULL; } elseif ($mode == 'aggressive') { // let the loc-type-id be same as that of other-contact & go ahead // with merge assuming aggressive mode continue; } } } elseif ($migrationInfo['rows'][$key]['main'] == $migrationInfo['rows'][$key]['other']) { // for loc blocks other than address like email, phone .. if values are same no point in merging // and adding redundant value unset($migrationInfo[$key]); } } } // A hook to implement other algorithms for choosing which contact to bias to when // there's a conflict (to handle "gotchas"). fields_in_conflict could be modified here // merge happens with new values filled in here. For a particular field / row not to be merged // field should be unset from fields_in_conflict. $migrationData['fields_in_conflict'] = $conflicts; CRM_Utils_Hook::merge('batch', $migrationData, $mainId, $otherId); $conflicts = $migrationData['fields_in_conflict']; if (!empty($conflicts)) { foreach ($conflicts as $key => $val) { if ($val === NULL and $mode == 'safe') { // un-resolved conflicts still present. Lets skip this merge. return TRUE; } else { // copy over the resolved values $migrationInfo[$key] = $val; } } } return FALSE; }
public function postProcess() { $formValues = $this->exportValues(); // reset all selected contact ids from session // when we came from search context, CRM-3526 $session = CRM_Core_Session::singleton(); if ($session->get('selectedSearchContactIds')) { $session->resetScope('selectedSearchContactIds'); } $formValues['main_details'] = $this->_mainDetails; $formValues['other_details'] = $this->_otherDetails; $migrationData = array('migration_info' => $formValues); CRM_Utils_Hook::merge('form', $migrationData, $this->_cid, $this->_oid); CRM_Dedupe_Merger::moveAllBelongings($this->_cid, $this->_oid, $migrationData['migration_info']); $name = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $this->_cid, 'display_name'); $message = '<ul><li>' . ts('%1 has been updated.', array(1 => $name)) . '</li><li>' . ts('Contact ID %1 has been deleted.', array(1 => $this->_oid)) . '</li></ul>'; CRM_Core_Session::setStatus($message, ts('Contacts Merged'), 'success'); $url = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->_cid}"); $urlParams = "reset=1&gid={$this->_gid}&rgid={$this->_rgid}&limit={$this->limit}"; if (!empty($formValues['_qf_Merge_submit'])) { $urlParams .= "&action=update"; $lisitingURL = CRM_Utils_System::url('civicrm/contact/dedupefind', $urlParams); CRM_Utils_System::redirect($lisitingURL); } if (!empty($formValues['_qf_Merge_done'])) { CRM_Utils_System::redirect($url); } if ($this->next && $this->_mergeId) { $cacheKey = CRM_Dedupe_Merger::getMergeCacheKeyString($this->_rgid, $this->_gid); $join = CRM_Dedupe_Merger::getJoinOnDedupeTable(); $where = "de.id IS NULL"; $pos = CRM_Core_BAO_PrevNextCache::getPositions($cacheKey, NULL, NULL, $this->_mergeId, $join, $where); if (!empty($pos) && $pos['next']['id1'] && $pos['next']['id2']) { $urlParams .= "&cid={$pos['next']['id1']}&oid={$pos['next']['id2']}&mergeId={$pos['next']['mergeId']}&action=update"; $url = CRM_Utils_System::url('civicrm/contact/merge', $urlParams); } } CRM_Utils_System::redirect($url); }
/** * Based on the provided two contact_ids and a set of tables, move the * belongings of the other contact to the main one. */ function moveContactBelongings($mainId, $otherId, $tables = false, $tableOperations = array()) { $cidRefs = self::cidRefs(); $eidRefs = self::eidRefs(); $cpTables = self::cpTables(); $paymentTables = self::paymentTables(); $affected = array_merge(array_keys($cidRefs), array_keys($eidRefs)); if ($tables !== false) { // if there are specific tables, sanitize the list $affected = array_unique(array_intersect($affected, $tables)); } else { // if there aren't any specific tables, don't affect the ones handled by relTables() $relTables =& self::relTables(); $handled = array(); foreach ($relTables as $params) { $handled = array_merge($handled, $params['tables']); } $affected = array_diff($affected, $handled); } $mainId = (int) $mainId; $otherId = (int) $otherId; // use UPDATE IGNORE + DELETE query pair to skip on situations when // there's a UNIQUE restriction on ($field, some_other_field) pair $sqls = array(); foreach ($affected as $table) { //here we require custom processing. if (array_key_exists($table, $cpTables)) { $path = CRM_Utils_Array::value('path', $cpTables[$table]); $fName = CRM_Utils_Array::value('function', $cpTables[$table]); if ($path && $fName) { require_once str_replace('_', DIRECTORY_SEPARATOR, $path) . ".php"; eval("{$path}::{$fName}( {$mainId}, null, {$otherId} );"); } continue; } if (isset($cidRefs[$table])) { foreach ($cidRefs[$table] as $field) { // carry related contributions CRM-5359 if (in_array($table, $paymentTables)) { $payOprSqls = self::operationSql($mainId, $otherId, $table, $tableOperations, 'payment'); $sqls = array_merge($sqls, $payOprSqls); $paymentSqls = self::paymentSql($table, $mainId, $otherId); $sqls = array_merge($sqls, $paymentSqls); } $preOperationSqls = self::operationSql($mainId, $otherId, $table, $tableOperations); $sqls = array_merge($sqls, $preOperationSqls); $sqls[] = "UPDATE IGNORE {$table} SET {$field} = {$mainId} WHERE {$field} = {$otherId}"; $sqls[] = "DELETE FROM {$table} WHERE {$field} = {$otherId}"; } } if (isset($eidRefs[$table])) { foreach ($eidRefs[$table] as $entityTable => $entityId) { $sqls[] = "UPDATE IGNORE {$table} SET {$entityId} = {$mainId} WHERE {$entityId} = {$otherId} AND {$entityTable} = 'civicrm_contact'"; $sqls[] = "DELETE FROM {$table} WHERE {$entityId} = {$otherId} AND {$entityTable} = 'civicrm_contact'"; } } } // CRM-6184: if we’re moving relationships, update civicrm_contact.employer_id if (is_array($tables) and in_array('civicrm_relationship', $tables)) { $sqls[] = "UPDATE IGNORE civicrm_contact SET employer_id = {$mainId} WHERE employer_id = {$otherId}"; } // Allow hook_civicrm_merge() to add SQL statements for the merge operation. CRM_Utils_Hook::merge('sqls', $sqls, $mainId, $otherId, $tables); // call the SQL queries in one transaction require_once 'CRM/Core/Transaction.php'; $transaction = new CRM_Core_Transaction(); foreach ($sqls as $sql) { CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray, true, null, true); } $transaction->commit(); }
/** * Merge two tags: tag B into tag A. */ public function mergeTags($tagAId, $tagBId) { $queryParams = array(1 => array($tagBId, 'Integer'), 2 => array($tagAId, 'Integer')); // re-compute used_for field $query = "SELECT id, name, used_for FROM civicrm_tag WHERE id IN (%1, %2)"; $dao = CRM_Core_DAO::executeQuery($query, $queryParams); $tags = array(); while ($dao->fetch()) { $label = $dao->id == $tagAId ? 'tagA' : 'tagB'; $tags[$label] = $dao->name; $tags["{$label}_used_for"] = $dao->used_for ? explode(",", $dao->used_for) : array(); } $usedFor = array_merge($tags["tagA_used_for"], $tags["tagB_used_for"]); $usedFor = implode(',', array_unique($usedFor)); $tags["tagB_used_for"] = explode(",", $usedFor); // get all merge queries together $sqls = array("UPDATE IGNORE civicrm_entity_tag SET tag_id = %1 WHERE tag_id = %2", "UPDATE civicrm_tag SET used_for = '{$usedFor}' WHERE id = %1", "DELETE FROM civicrm_tag WHERE id = %2", "DELETE et2.* from civicrm_entity_tag et1 INNER JOIN civicrm_entity_tag et2 ON et1.entity_table = et2.entity_table AND et1.entity_id = et2.entity_id AND et1.tag_id = et2.tag_id WHERE et1.id < et2.id", "DELETE FROM civicrm_entity_tag WHERE tag_id = %2"); $tables = array('civicrm_entity_tag', 'civicrm_tag'); // Allow hook_civicrm_merge() to add SQL statements for the merge operation AND / OR // perform any other actions like logging CRM_Utils_Hook::merge('sqls', $sqls, $tagAId, $tagBId, $tables); // call the SQL queries in one transaction $transaction = new CRM_Core_Transaction(); foreach ($sqls as $sql) { CRM_Core_DAO::executeQuery($sql, $queryParams, TRUE, NULL, TRUE); } $transaction->commit(); $tags['status'] = TRUE; return $tags; }
/** * A function which uses various rules / algorithms for choosing which contact to bias to * when there's a conflict (to handle "gotchas"). Plus the safest route to merge. * * @param int $mainId * Main contact with whom merge has to happen. * @param int $otherId * Duplicate contact which would be deleted after merge operation. * @param array $migrationInfo * Array of information about which elements to merge. * @param string $mode * Helps decide how to behave when there are conflicts. * - A 'safe' value skips the merge if there are any un-resolved conflicts. * - Does a force merge otherwise (aggressive mode). * * @param array $conflicts * * @return bool */ public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe', &$conflicts = array()) { $originalMigrationInfo = $migrationInfo; foreach ($migrationInfo as $key => $val) { if ($val === "null") { // Rule: Never overwrite with an empty value (in any mode) unset($migrationInfo[$key]); continue; } elseif ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) or substr($key, 0, 12) == 'move_custom_') and $val != NULL) { // Rule: If both main-contact, and other-contact have a field with a // different value, then let $mode decide if to merge it or not if ((!empty($migrationInfo['rows'][$key]['main']) || $migrationInfo['rows'][$key]['main'] === '0' && substr($key, 0, 12) == 'move_custom_') && $migrationInfo['rows'][$key]['main'] != $migrationInfo['rows'][$key]['other']) { // note it down & lets wait for response from the hook. // For no response $mode will decide if to skip this merge $conflicts[$key] = NULL; } } elseif (substr($key, 0, 14) == 'move_location_' and $val != NULL) { $locField = explode('_', $key); $fieldName = $locField[2]; $fieldCount = $locField[3]; // Rule: Catch address conflicts (same address type on both contacts) if (isset($migrationInfo['main_details']['location_blocks'][$fieldName]) && !empty($migrationInfo['main_details']['location_blocks'][$fieldName])) { // Load the address we're inspecting from the 'other' contact $addressRecord = $migrationInfo['other_details']['location_blocks'][$fieldName][$fieldCount]; $addressRecordLocTypeId = CRM_Utils_Array::value('location_type_id', $addressRecord); // If it exists on the 'main' contact already, skip it. Otherwise // if the location type exists already, log a conflict. foreach ($migrationInfo['main_details']['location_blocks'][$fieldName] as $mainAddressKey => $mainAddressRecord) { if (self::locationIsSame($addressRecord, $mainAddressRecord)) { unset($migrationInfo[$key]); break; } elseif ($addressRecordLocTypeId == $mainAddressRecord['location_type_id']) { $conflicts[$key] = NULL; break; } } } elseif (CRM_Utils_Array::value('main', $migrationInfo['rows'][$key]) == $migrationInfo['rows'][$key]['other']) { unset($migrationInfo[$key]); } } } // A hook to implement other algorithms for choosing which contact to bias to when // there's a conflict (to handle "gotchas"). fields_in_conflict could be modified here // merge happens with new values filled in here. For a particular field / row not to be merged // field should be unset from fields_in_conflict. $migrationData = array('old_migration_info' => $originalMigrationInfo, 'mode' => $mode, 'fields_in_conflict' => $conflicts, 'merge_mode' => $mode, 'migration_info' => $migrationInfo); CRM_Utils_Hook::merge('batch', $migrationData, $mainId, $otherId); $conflicts = $migrationData['fields_in_conflict']; // allow hook to override / manipulate migrationInfo as well $migrationInfo = $migrationData['migration_info']; if (!empty($conflicts)) { foreach ($conflicts as $key => $val) { if ($val === NULL and $mode == 'safe') { // un-resolved conflicts still present. Lets skip this merge after saving the conflict / reason. return TRUE; } else { // copy over the resolved values $migrationInfo[$key] = $val; } } // if there are conflicts and mode is aggressive, allow hooks to decide if to skip merges if (array_key_exists('skip_merge', $migrationData)) { return (bool) $migrationData['skip_merge']; } } return FALSE; }