<?php
/*
+--------------------------------------------------------------------+
- | CiviCRM version 5 |
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2020 |
- +--------------------------------------------------------------------+
- | This file is a part of CiviCRM. |
- | |
- | CiviCRM is free software; you can copy, modify, and distribute it |
- | under the terms of the GNU Affero General Public License |
- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | Copyright CiviCRM LLC. All rights reserved. |
| |
- | CiviCRM is distributed in the hope that it will be useful, but |
- | WITHOUT ANY WARRANTY; without even the implied warranty of |
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
- | See the GNU Affero General Public License for more details. |
- | |
- | You should have received a copy of the GNU Affero General Public |
- | License and the CiviCRM Licensing Exception along |
- | with this program; if not, contact CiviCRM LLC |
- | at info[AT]civicrm[DOT]org. If you have questions about the |
- | GNU Affero General Public License or the licensing of CiviCRM, |
- | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
- * @copyright CiviCRM LLC (c) 2004-2020
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Dedupe_Merger {
public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe', &$conflicts = []) {
$conflicts = self::getConflicts($migrationInfo, $mainId, $otherId, $mode);
-
- if (!empty($conflicts)) {
- // if there are conflicts and mode is aggressive, allow hooks to decide if to skip merges
- return (bool) $migrationInfo['skip_merge'];
- }
- return FALSE;
+ // A hook could have set skip_merge in order to alter merge behaviour.
+ // This is a something we might ideally deprecate since they really 'should'
+ // mess with the conflicts array instead.
+ return (bool) ($migrationData['skip_merge'] ?? !empty($conflicts));
}
/**
// CRM-15681 don't display sub-types in UI
continue;
}
- $rows["move_$field"]['main'] = self::getFieldValueAndLabel($field, $main)['label'];
- $rows["move_$field"]['other'] = self::getFieldValueAndLabel($field, $other)['label'];
+ $rows["move_$field"] = [
+ 'main' => self::getFieldValueAndLabel($field, $main)['label'],
+ 'other' => self::getFieldValueAndLabel($field, $other)['label'],
+ 'title' => $fields[$field]['title'],
+ ];
$value = self::getFieldValueAndLabel($field, $other)['value'];
//CRM-14334
}
$migrationInfo["move_$field"] = $value;
- $rows["move_$field"]['title'] = $fields[$field]['title'];
}
// Handle location blocks.
}
// Provide a select drop-down for the location's type/provider
- // eg websites: Google+, Facebook...
+ // eg websites: Facebook...
if ($blockInfo['hasType']) {
}
}
- $relTables = CRM_Dedupe_Merger::relTables();
- $activeRelTables = CRM_Dedupe_Merger::getActiveRelTables($otherId);
- $activeMainRelTables = CRM_Dedupe_Merger::getActiveRelTables($mainId);
+ $mergeHandler = new CRM_Dedupe_MergeHandler((int) $mainId, (int) $otherId);
+ $relTables = $mergeHandler->getTablesRelatedToTheMergePair();
foreach ($relTables as $name => $null) {
- if (!in_array($name, $activeRelTables) &&
- !(($name == 'rel_table_users') && in_array($name, $activeMainRelTables))
- ) {
- unset($relTables[$name]);
- continue;
- }
-
$relTableElements[] = ['checkbox', "move_$name"];
$migrationInfo["move_$name"] = 1;
);
foreach ($otherTree as $gid => $group) {
- $foundField = FALSE;
if (!isset($group['fields'])) {
continue;
}
foreach ($group['fields'] as $fid => $field) {
+ $mainContactValue = $mainTree[$gid]['fields'][$fid]['customValue'] ?? NULL;
+ $otherContactValue = $otherTree[$gid]['fields'][$fid]['customValue'] ?? NULL;
if (in_array($fid, $compareFields['custom'])) {
- if (!$foundField) {
- $rows["custom_group_$gid"]['title'] = $group['title'];
- $foundField = TRUE;
- }
- if (!empty($mainTree[$gid]['fields'][$fid]['customValue'])) {
- foreach ($mainTree[$gid]['fields'][$fid]['customValue'] as $valueId => $values) {
+ $rows["custom_group_$gid"]['title'] = $rows["custom_group_$gid"]['title'] ?? $group['title'];
+
+ if ($mainContactValue) {
+ foreach ($mainContactValue as $valueId => $values) {
$rows["move_custom_$fid"]['main'] = CRM_Core_BAO_CustomField::displayValue($values['data'], $fid);
}
}
- $value = "null";
- if (!empty($otherTree[$gid]['fields'][$fid]['customValue'])) {
- foreach ($otherTree[$gid]['fields'][$fid]['customValue'] as $valueId => $values) {
+ $value = 'null';
+ if ($otherContactValue) {
+ foreach ($otherContactValue as $valueId => $values) {
$rows["move_custom_$fid"]['other'] = CRM_Core_BAO_CustomField::displayValue($values['data'], $fid);
if ($values['data'] === 0 || $values['data'] === '0') {
$values['data'] = $qfZeroBug;
}
$migrationInfo = [];
$conflicts = [];
+ // Try to lock the contacts before we load the data as we don't want it changing under us.
+ // https://lab.civicrm.org/dev/core/issues/1355
+ $locks = self::getLocksOnContacts([$mainId, $otherId]);
if (!CRM_Dedupe_Merger::skipMerge($mainId, $otherId, $migrationInfo, $mode, $conflicts)) {
CRM_Dedupe_Merger::moveAllBelongings($mainId, $otherId, $migrationInfo, $checkPermissions);
$resultStats['merged'][] = [
else {
CRM_Core_BAO_PrevNextCache::deletePair($mainId, $otherId, $cacheKeyString);
}
+ self::releaseLocks($locks);
return $resultStats;
}
unset($conflicts[$key]);
}
}
- $migrationInfo['skip_merge'] = $migrationData['skip_merge'] ?? !empty($conflicts);
return self::formatConflictArray($conflicts, $migrationInfo['rows'], $migrationInfo['main_details']['location_blocks'], $migrationInfo['other_details']['location_blocks'], $mainId, $otherId);
}
return $locationBlock;
}
+ /**
+ * Get a lock on the given contact.
+ *
+ * The lock is like a gentleman's agreement between php & mysql. It is reserved at the
+ * mysql level so it works across php processes but it doesn't actually lock the database.
+ *
+ * Instead php can check the lock to see if it has been acquired before taking an action.
+ *
+ * In this case we really don't want to attempt to dedupe contacts if another process is
+ * trying to act on the specific contact as it could result in messy deadlocks & possibly data corruption.
+ * In most databases this would be a rare event but if multiple dedupe processes are running
+ * at once (for example) or there is also an import process in play there is potential for them to crash.
+ * By throwing a specific error the calling process can catch it and determine it is worth trying again later without a lot of
+ * noise.
+ *
+ * As of writing no other processes DO grab contact locks but it would be reasonable to consider
+ * grabbing them doing contact edits in general as well as imports etc.
+ *
+ * @param array $contacts
+ *
+ * @return array
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \CRM_Core_Exception_ResourceConflictException
+ */
+ protected static function getLocksOnContacts($contacts):array {
+ $locks = [];
+ if (!CRM_Utils_SQL::supportsMultipleLocks()) {
+ return $locks;
+ }
+ foreach ($contacts as $contactID) {
+ $lock = Civi::lockManager()->acquire("data.core.contact.{$contactID}");
+ if ($lock->isAcquired()) {
+ $locks[] = $lock;
+ }
+ else {
+ self::releaseLocks($locks);
+ throw new CRM_Core_Exception_ResourceConflictException(ts('Contact is in Use'), 'contact_lock');
+ }
+ }
+ return $locks;
+ }
+
+ /**
+ * Release contact locks so another process can alter them if it wants.
+ *
+ * @param array $locks
+ */
+ protected static function releaseLocks(array $locks) {
+ foreach ($locks as $lock) {
+ /* @var Civi\Core\Lock\LockInterface $lock */
+ $lock->release();
+ }
+ }
+
}