Merge pull request #15978 from civicrm/5.20
[civicrm-core.git] / CRM / Contact / Form / Merge.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * Class CRM_Contact_Form_Merge.
20 */
21 class CRM_Contact_Form_Merge extends CRM_Core_Form {
22 // The id of the contact that there's a duplicate for; this one will
23 /**
24 * possibly inherit some of $_oid's properties and remain in the system.
25 * @var int
26 */
27 public $_cid = NULL;
28
29 /**
30 * The id of the other contact - the duplicate one that will get deleted.
31 * @var int
32 */
33 public $_oid = NULL;
34
35 public $_contactType = NULL;
36
37 /**
38 * @var array
39 */
40 public $criteria = [];
41
42 /**
43 * Query limit to be retained in the urls.
44 *
45 * @var int
46 */
47 public $limit;
48
49 /**
50 * String for quickform bug handling.
51 *
52 * FIXME: QuickForm can't create advcheckboxes with value set to 0 or '0' :(
53 * see HTML_QuickForm_advcheckbox::setValues() - but patching that doesn't
54 * help, as QF doesn't put the 0-value elements in exportValues() anyway...
55 * to side-step this, we use the below UUID as a (re)placeholder
56 *
57 * @var string
58 */
59 public $_qfZeroBug = 'e8cddb72-a257-11dc-b9cc-0016d3330ee9';
60
61 public function preProcess() {
62 try {
63
64 $this->_cid = CRM_Utils_Request::retrieve('cid', 'Positive', $this, TRUE);
65 $this->_oid = CRM_Utils_Request::retrieve('oid', 'Positive', $this, TRUE);
66 $flip = CRM_Utils_Request::retrieve('flip', 'Positive', $this, FALSE);
67
68 $this->_rgid = CRM_Utils_Request::retrieve('rgid', 'Positive', $this, FALSE);
69 $this->_gid = $gid = CRM_Utils_Request::retrieve('gid', 'Positive', $this, FALSE);
70 $this->_mergeId = CRM_Utils_Request::retrieve('mergeId', 'Positive', $this, FALSE);
71 $this->limit = CRM_Utils_Request::retrieve('limit', 'Positive', $this, FALSE);
72 $this->criteria = CRM_Utils_Request::retrieve('criteria', 'Json', $this, FALSE, '{}');
73
74 $urlParams = ['reset' => 1, 'rgid' => $this->_rgid, 'gid' => $this->_gid, 'limit' => $this->limit, 'criteria' => $this->criteria];
75
76 $this->bounceIfInvalid($this->_cid, $this->_oid);
77
78 $contacts = civicrm_api3('Contact', 'get', [
79 'id' => ['IN' => [$this->_cid, $this->_oid]],
80 'return' => ['contact_type', 'modified_date', 'created_date', 'contact_sub_type'],
81 ])['values'];
82
83 $this->_contactType = $contacts[$this->_cid]['contact_type'];
84
85 $browseUrl = CRM_Utils_System::url('civicrm/contact/dedupefind', array_merge($urlParams, ['action' => 'browse']));
86
87 if (!$this->_rgid) {
88 // Unset browse URL as we have come from the search screen.
89 $browseUrl = '';
90 $this->_rgid = civicrm_api3('RuleGroup', 'getvalue', [
91 'contact_type' => $this->_contactType,
92 'used' => 'Supervised',
93 'return' => 'id',
94 ]);
95 }
96 $this->assign('browseUrl', $browseUrl);
97 if ($browseUrl) {
98 CRM_Core_Session::singleton()->pushUserContext($browseUrl);
99 }
100
101 $cacheKey = CRM_Dedupe_Merger::getMergeCacheKeyString($this->_rgid, $gid, json_decode($this->criteria, TRUE), TRUE, $this->limit);
102
103 $join = CRM_Dedupe_Merger::getJoinOnDedupeTable();
104 $where = "de.id IS NULL";
105
106 $pos = CRM_Core_BAO_PrevNextCache::getPositions($cacheKey, $this->_cid, $this->_oid, $this->_mergeId, $join, $where, $flip);
107
108 // get user info of main contact.
109 $config = CRM_Core_Config::singleton();
110 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
111
112 $mainUfId = CRM_Core_BAO_UFMatch::getUFId($this->_cid);
113 $mainUser = NULL;
114 if ($mainUfId) {
115 $mainUser = $config->userSystem->getUser($this->_cid);
116 $this->assign('mainUfId', $mainUfId);
117 $this->assign('mainUfName', $mainUser ? $mainUser['name'] : NULL);
118 }
119 $flipParams = array_merge($urlParams, ['action' => 'update', 'cid' => $this->_oid, 'oid' => $this->_cid]);
120 if (!$flip) {
121 $flipParams['flip'] = '1';
122 }
123 $flipUrl = CRM_Utils_System::url('civicrm/contact/merge',
124 $flipParams
125 );
126 $this->assign('flip', $flipUrl);
127
128 $this->prev = $this->next = NULL;
129 foreach ([
130 'prev',
131 'next',
132 ] as $position) {
133 if (!empty($pos[$position])) {
134 if ($pos[$position]['id1'] && $pos[$position]['id2']) {
135 $rowParams = array_merge($urlParams, [
136 'action' => 'update',
137 'cid' => $pos[$position]['id1'],
138 'oid' => $pos[$position]['id2'],
139 'mergeId' => $pos[$position]['mergeId'],
140 ]);
141 $this->$position = CRM_Utils_System::url('civicrm/contact/merge', $rowParams);
142 $this->assign($position, $this->$position);
143 }
144 }
145 }
146
147 // get user info of other contact.
148 $otherUfId = CRM_Core_BAO_UFMatch::getUFId($this->_oid);
149 $otherUser = NULL;
150
151 if ($otherUfId) {
152 $otherUser = $config->userSystem->getUser($this->_oid);
153 $this->assign('otherUfId', $otherUfId);
154 $this->assign('otherUfName', $otherUser ? $otherUser['name'] : NULL);
155 }
156
157 $cmsUser = ($mainUfId && $otherUfId) ? TRUE : FALSE;
158 $this->assign('user', $cmsUser);
159
160 $rowsElementsAndInfo = CRM_Dedupe_Merger::getRowsElementsAndInfo($this->_cid, $this->_oid);
161 $main = $this->_mainDetails = $rowsElementsAndInfo['main_details'];
162 $other = $this->_otherDetails = $rowsElementsAndInfo['other_details'];
163
164 $this->assign('contact_type', $main['contact_type']);
165 $this->assign('main_name', $main['display_name']);
166 $this->assign('other_name', $other['display_name']);
167 $this->assign('main_cid', $main['contact_id']);
168 $this->assign('other_cid', $other['contact_id']);
169 $this->assign('rgid', $this->_rgid);
170 $this->assignSummaryRowsToTemplate($contacts);
171
172 $this->addElement('checkbox', 'toggleSelect', NULL, NULL, ['class' => 'select-rows']);
173
174 $this->assign('mainLocBlock', json_encode($rowsElementsAndInfo['main_details']['location_blocks']));
175 $this->assign('locationBlockInfo', json_encode(CRM_Dedupe_Merger::getLocationBlockInfo()));
176 $this->assign('mainContactTypeIcon', CRM_Contact_BAO_Contact_Utils::getImage($contacts[$this->_cid]['contact_sub_type'] ? $contacts[$this->_cid]['contact_sub_type'] : $contacts[$this->_cid]['contact_type'],
177 FALSE,
178 $this->_cid
179 ));
180 $this->assign('otherContactTypeIcon', CRM_Contact_BAO_Contact_Utils::getImage($contacts[$this->_oid]['contact_sub_type'] ? $contacts[$this->_oid]['contact_sub_type'] : $contacts[$this->_oid]['contact_type'],
181 FALSE,
182 $this->_oid
183 ));
184
185 if (isset($rowsElementsAndInfo['rows']['move_contact_type'])) {
186 // We don't permit merging contacts of different types so this is just clutter - putting
187 // the icon next to the contact name is consistent with elsewhere and permits hover-info
188 // https://lab.civicrm.org/dev/core/issues/824
189 unset($rowsElementsAndInfo['rows']['move_contact_type']);
190 }
191
192 $this->assign('rows', $rowsElementsAndInfo['rows']);
193
194 // add elements
195 foreach ($rowsElementsAndInfo['elements'] as $element) {
196 // We could push this down to the getRowsElementsAndInfo function but it's
197 // already so overloaded - let's start moving towards doing form-things
198 // on the form.
199 if (substr($element[1], 0, 13) === 'move_location') {
200 $element[4] = array_merge(
201 (array) CRM_Utils_Array::value(4, $element, []),
202 [
203 'data-location' => substr($element[1], 14),
204 'data-is_location' => TRUE,
205 ]);
206 }
207 if (substr($element[1], 0, 15) === 'location_blocks') {
208 // @todo We could add some data elements here to make jquery manipulation more straight-forward
209 // @todo consider enabling if it is an add & defaulting to true.
210 $element[4] = array_merge((array) CRM_Utils_Array::value(4, $element, []), ['disabled' => TRUE]);
211 }
212 $newCheckBox = $this->addElement($element[0],
213 $element[1],
214 array_key_exists('2', $element) ? $element[2] : NULL,
215 array_key_exists('3', $element) ? $element[3] : NULL,
216 array_key_exists('4', $element) ? $element[4] : NULL,
217 array_key_exists('5', $element) ? $element[5] : NULL
218 );
219 if (!empty($element['is_checked'])) {
220 $newCheckBox->setChecked(TRUE);
221 }
222 }
223
224 // add related table elements
225 foreach ($rowsElementsAndInfo['rel_table_elements'] as $relTableElement) {
226 $element = $this->addElement($relTableElement[0], $relTableElement[1]);
227 $element->setChecked(TRUE);
228 }
229
230 $this->assign('rel_tables', $rowsElementsAndInfo['rel_tables']);
231 $this->assign('userContextURL', CRM_Core_Session::singleton()
232 ->readUserContext());
233 }
234 catch (CRM_Core_Exception $e) {
235 CRM_Core_Error::statusBounce($e->getMessage());
236 }
237 }
238
239 public function addRules() {
240 }
241
242 public function buildQuickForm() {
243 $this->unsavedChangesWarn = FALSE;
244 CRM_Utils_System::setTitle(ts('Merge %1 contacts', [1 => $this->_contactType]));
245 $buttons = [];
246
247 $buttons[] = [
248 'type' => 'next',
249 'name' => $this->next ? ts('Merge and go to Next Pair') : ts('Merge'),
250 'isDefault' => TRUE,
251 'icon' => $this->next ? 'fa-play-circle' : 'check',
252 ];
253
254 if ($this->next || $this->prev) {
255 $buttons[] = [
256 'type' => 'submit',
257 'name' => ts('Merge and go to Listing'),
258 ];
259 $buttons[] = [
260 'type' => 'done',
261 'name' => ts('Merge and View Result'),
262 'icon' => 'fa-check-circle',
263 ];
264 }
265
266 $buttons[] = [
267 'type' => 'cancel',
268 'name' => ts('Cancel'),
269 ];
270
271 $this->addButtons($buttons);
272 $this->addFormRule(['CRM_Contact_Form_Merge', 'formRule'], $this);
273 }
274
275 /**
276 * @param $fields
277 * @param $files
278 * @param $self
279 *
280 * @return array
281 */
282 public static function formRule($fields, $files, $self) {
283 $errors = [];
284 $link = CRM_Utils_System::href(ts('Flip between the original and duplicate contacts.'),
285 'civicrm/contact/merge',
286 'reset=1&action=update&cid=' . $self->_oid . '&oid=' . $self->_cid . '&rgid=' . $self->_rgid . '&flip=1'
287 );
288 if (CRM_Contact_BAO_Contact::checkDomainContact($self->_oid)) {
289 $errors['_qf_default'] = ts("The Default Organization contact cannot be merged into another contact record. It is associated with the CiviCRM installation for this domain and contains information used for system functions. If you want to merge these records, you can: %1", [1 => $link]);
290 }
291 return $errors;
292 }
293
294 public function postProcess() {
295 $formValues = $this->exportValues();
296
297 $formValues['main_details'] = $this->_mainDetails;
298 $formValues['other_details'] = $this->_otherDetails;
299 $migrationData = ['migration_info' => $formValues];
300 CRM_Utils_Hook::merge('form', $migrationData, $this->_cid, $this->_oid);
301 CRM_Dedupe_Merger::moveAllBelongings($this->_cid, $this->_oid, $migrationData['migration_info']);
302
303 $name = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $this->_cid, 'display_name');
304 $message = '<ul><li>' . ts('%1 has been updated.', [1 => $name]) . '</li><li>' . ts('Contact ID %1 has been deleted.', [1 => $this->_oid]) . '</li></ul>';
305 CRM_Core_Session::setStatus($message, ts('Contacts Merged'), 'success');
306
307 $urlParams = ['reset' => 1, 'cid' => $this->_cid, 'rgid' => $this->_rgid, 'gid' => $this->_gid, 'limit' => $this->limit, 'criteria' => $this->criteria];
308 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $this->_cid]);
309
310 if (!empty($formValues['_qf_Merge_submit'])) {
311 $urlParams['action'] = "update";
312 CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/dedupefind',
313 $urlParams
314 ));
315 }
316 if (!empty($formValues['_qf_Merge_done'])) {
317 CRM_Utils_System::redirect($contactViewUrl);
318 }
319
320 if ($this->next && $this->_mergeId) {
321 $cacheKey = CRM_Dedupe_Merger::getMergeCacheKeyString($this->_rgid, $this->_gid, json_decode($this->criteria, TRUE), TRUE, $this->limit);
322
323 $join = CRM_Dedupe_Merger::getJoinOnDedupeTable();
324 $where = "de.id IS NULL";
325
326 $pos = CRM_Core_BAO_PrevNextCache::getPositions($cacheKey, NULL, NULL, $this->_mergeId, $join, $where);
327
328 if (!empty($pos) &&
329 $pos['next']['id1'] &&
330 $pos['next']['id2']
331 ) {
332
333 $urlParams['cid'] = $pos['next']['id1'];
334 $urlParams['oid'] = $pos['next']['id2'];
335 $urlParams['mergeId'] = $pos['next']['mergeId'];
336 $urlParams['action'] = 'update';
337 CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/merge', $urlParams));
338 }
339 }
340
341 // Perhaps never reached.
342 CRM_Utils_System::redirect($contactViewUrl);
343 }
344
345 /**
346 * Bounce if the merge action is invalid.
347 *
348 * We don't allow the merge if it is nonsensical, marked as a duplicate
349 * or outside the user's permission.
350 *
351 * @param int $cid
352 * Contact ID to retain
353 * @param int $oid
354 * Contact ID to delete.
355 */
356 public function bounceIfInvalid($cid, $oid) {
357 if ($cid == $oid) {
358 CRM_Core_Error::statusBounce(ts('Cannot merge a contact with itself.'));
359 }
360
361 if (!CRM_Dedupe_BAO_Rule::validateContacts($cid, $oid)) {
362 CRM_Core_Error::statusBounce(ts('The selected pair of contacts are marked as non duplicates. If these records should be merged, you can remove this exception on the <a href="%1">Dedupe Exceptions</a> page.', [1 => CRM_Utils_System::url('civicrm/dedupe/exception', 'reset=1')]));
363 }
364
365 if (!(CRM_Contact_BAO_Contact_Permission::allow($cid, CRM_Core_Permission::EDIT) &&
366 CRM_Contact_BAO_Contact_Permission::allow($oid, CRM_Core_Permission::EDIT)
367 )
368 ) {
369 CRM_Utils_System::permissionDenied();
370 }
371 // ensure that oid is not the current user, if so refuse to do the merge
372 if (CRM_Core_Session::singleton()->getLoggedInContactID() == $oid) {
373 $message = ts('The contact record which is linked to the currently logged in user account - \'%1\' - cannot be deleted.',
374 [1 => CRM_Core_Session::singleton()->getLoggedInContactDisplayName()]
375 );
376 CRM_Core_Error::statusBounce($message);
377 }
378 }
379
380 /**
381 * Assign the summary_rows variable to the tpl.
382 *
383 * This adds rows to the beginning of the block that will help in making merge choices.
384 *
385 * It can be modified by a hook by altering what is assigned. Although not technically supported this
386 * is an easy tweak with no earth-shattering impacts if later changes stop if from working.
387 *
388 * https://lab.civicrm.org/dev/core/issues/824
389 *
390 * @param array $contacts
391 */
392 protected function assignSummaryRowsToTemplate($contacts) {
393 $mostRecent = ($contacts[$this->_cid]['modified_date'] < $contacts[$this->_oid]['modified_date']) ? $this->_oid : $this->_cid;
394 $this->assign('summary_rows', [
395 [
396 'name' => 'created_date',
397 'label' => ts('Created'),
398 'main_contact_value' => CRM_Utils_Date::customFormat($contacts[$this->_cid]['created_date']),
399 'other_contact_value' => CRM_Utils_Date::customFormat($contacts[$this->_oid]['created_date']),
400 ],
401 [
402 'name' => 'modified_date',
403 'label' => ts('Last Modified'),
404 'main_contact_value' => CRM_Utils_Date::customFormat($contacts[$this->_cid]['modified_date']) . ($mostRecent == $this->_cid ? ' (' . ts('Most Recent') . ')' : ''),
405 'other_contact_value' => CRM_Utils_Date::customFormat($contacts[$this->_oid]['modified_date']) . ($mostRecent == $this->_oid ? ' (' . ts('Most Recent') . ')' : ''),
406 ],
407 ]);
408 }
409
410 }