Merge pull request #16888 from mattwire/addvarsanyregion
[civicrm-core.git] / CRM / Contact / Page / AJAX.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 *
17 */
18
19/**
20 * This class contains all contact related functions that are called using AJAX (jQuery)
21 */
22class CRM_Contact_Page_AJAX {
272081ca
TO
23 /**
24 * When a user chooses a username, CHECK_USERNAME_TTL
25 * is the time window in which they can check usernames
26 * (without reloading the overall form).
27 */
69078420
SL
28 // 3hr; 3*60*60
29 const CHECK_USERNAME_TTL = 10800;
272081ca 30
69078420
SL
31 // 6hr; 6*60*60
32 const AUTOCOMPLETE_TTL = 21600;
b1dba111 33
06508628 34 /**
06508628
CW
35 * Ajax callback for custom fields of type ContactReference
36 *
37 * Todo: Migrate contact reference fields to use EntityRef
38 */
00be9182 39 public static function contactReference() {
9c1bc317 40 $name = $_GET['term'] ?? NULL;
6a488035
TO
41 $name = CRM_Utils_Type::escape($name, 'String');
42 $cfID = CRM_Utils_Type::escape($_GET['id'], 'Positive');
43
44 // check that this is a valid, active custom field of Contact Reference type
be2fb01f
CW
45 $params = ['id' => $cfID];
46 $returnProperties = ['filter', 'data_type', 'is_active'];
47 $cf = [];
6a488035 48 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_CustomField', $params, $cf, $returnProperties);
a4cce21a 49 if (!$cf['id'] || !$cf['is_active'] || $cf['data_type'] != 'ContactReference') {
d8cdb7e5 50 CRM_Utils_System::civiExit(1);
6a488035
TO
51 }
52
9624538c 53 if (!empty($cf['filter'])) {
be2fb01f 54 $filterParams = [];
6a488035
TO
55 parse_str($cf['filter'], $filterParams);
56
9c1bc317 57 $action = $filterParams['action'] ?? NULL;
be2fb01f 58 if (!empty($action) && !in_array($action, ['get', 'lookup'])) {
d8cdb7e5 59 CRM_Utils_System::civiExit(1);
6a488035 60 }
da0136df 61
62 if (!empty($filterParams['group'])) {
63 $filterParams['group'] = explode(',', $filterParams['group']);
64 }
6a488035
TO
65 }
66
67 $list = array_keys(CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
353ffa53
TO
68 'contact_reference_options'
69 ), '1');
6a488035 70
be2fb01f 71 $return = array_unique(array_merge(['sort_name'], $list));
6a488035 72
89595c92 73 $limit = Civi::settings()->get('search_autocomplete_count');
6a488035 74
be2fb01f 75 $params = ['offset' => 0, 'rowCount' => $limit, 'version' => 3];
6a488035
TO
76 foreach ($return as $fld) {
77 $params["return.{$fld}"] = 1;
78 }
79
80 if (!empty($action)) {
be2fb01f 81 $excludeGet = [
353ffa53
TO
82 'reset',
83 'key',
84 'className',
85 'fnName',
86 'json',
87 'reset',
88 'context',
89 'timestamp',
90 'limit',
91 'id',
92 's',
93 'q',
408b79bf 94 'action',
be2fb01f 95 ];
6a488035
TO
96 foreach ($_GET as $param => $val) {
97 if (empty($val) ||
98 in_array($param, $excludeGet) ||
99 strpos($param, 'return.') !== FALSE ||
100 strpos($param, 'api.') !== FALSE
101 ) {
102 continue;
103 }
104 $params[$param] = $val;
105 }
106 }
107
108 if ($name) {
109 $params['sort_name'] = $name;
110 }
111
112 $params['sort'] = 'sort_name';
113
114 // tell api to skip permission chk. dgg
115 $params['check_permissions'] = 0;
116
117 // add filter variable to params
118 if (!empty($filterParams)) {
119 $params = array_merge($params, $filterParams);
120 }
121
122 $contact = civicrm_api('Contact', 'Get', $params);
123
a7488080 124 if (!empty($contact['is_error'])) {
d8cdb7e5 125 CRM_Utils_System::civiExit(1);
6a488035
TO
126 }
127
be2fb01f 128 $contactList = [];
6a488035 129 foreach ($contact['values'] as $value) {
be2fb01f 130 $view = [];
6a488035 131 foreach ($return as $fld) {
a7488080 132 if (!empty($value[$fld])) {
6a488035
TO
133 $view[] = $value[$fld];
134 }
135 }
be2fb01f 136 $contactList[] = ['id' => $value['id'], 'text' => implode(' :: ', $view)];
6a488035
TO
137 }
138
da0136df 139 if (!empty($_GET['is_unit_test'])) {
140 return $contactList;
141 }
8be1a839 142 CRM_Utils_JSON::output($contactList);
6a488035
TO
143 }
144
145 /**
100fef9d 146 * Fetch PCP ID by PCP Supporter sort_name, also displays PCP title and associated Contribution Page title
6a488035 147 */
00be9182 148 public static function getPCPList() {
9c1bc317 149 $name = $_GET['term'] ?? NULL;
353ffa53 150 $name = CRM_Utils_Type::escape($name, 'String');
89595c92 151 $limit = $max = Civi::settings()->get('search_autocomplete_count');
6a488035
TO
152
153 $where = ' AND pcp.page_id = cp.id AND pcp.contact_id = cc.id';
154
155 $config = CRM_Core_Config::singleton();
156 if ($config->includeWildCardInName) {
157 $strSearch = "%$name%";
158 }
159 else {
160 $strSearch = "$name%";
161 }
162 $includeEmailFrom = $includeNickName = '';
163 if ($config->includeNickNameInName) {
164 $includeNickName = " OR nick_name LIKE '$strSearch'";
165 }
166 if ($config->includeEmailInName) {
167 $includeEmailFrom = "LEFT JOIN civicrm_email eml ON ( cc.id = eml.contact_id AND eml.is_primary = 1 )";
168 $whereClause = " WHERE ( email LIKE '$strSearch' OR sort_name LIKE '$strSearch' $includeNickName ) {$where} ";
169 }
170 else {
171 $whereClause = " WHERE ( sort_name LIKE '$strSearch' $includeNickName ) {$where} ";
172 }
173
8da35492 174 $offset = $count = 0;
4feb1cad
CW
175 if (!empty($_GET['page_num'])) {
176 $page = (int) $_GET['page_num'];
8da35492
CW
177 $offset = $limit * ($page - 1);
178 $limit++;
6a488035
TO
179 }
180
181 $select = 'cc.sort_name, pcp.title, cp.title';
182 $query = "
183 SELECT id, data
184 FROM (
185 SELECT pcp.id as id, CONCAT_WS( ' :: ', {$select} ) as data, sort_name
186 FROM civicrm_pcp pcp, civicrm_contribution_page cp, civicrm_contact cc
187 {$includeEmailFrom}
900e6829 188 {$whereClause} AND pcp.page_type = 'contribute'
189 UNION ALL
190 SELECT pcp.id as id, CONCAT_WS( ' :: ', {$select} ) as data, sort_name
191 FROM civicrm_pcp pcp, civicrm_event cp, civicrm_contact cc
192 {$includeEmailFrom}
193 {$whereClause} AND pcp.page_type = 'event'
6a488035
TO
194 ) t
195 ORDER BY sort_name
8da35492 196 LIMIT $offset, $limit
6a488035
TO
197 ";
198
199 $dao = CRM_Core_DAO::executeQuery($query);
be2fb01f 200 $output = ['results' => [], 'more' => FALSE];
6a488035 201 while ($dao->fetch()) {
8da35492
CW
202 if (++$count > $max) {
203 $output['more'] = TRUE;
204 }
205 else {
be2fb01f 206 $output['results'][] = ['id' => $dao->id, 'text' => $dao->data];
8da35492 207 }
6a488035 208 }
8da35492 209 CRM_Utils_JSON::output($output);
6a488035
TO
210 }
211
00be9182 212 public static function relationship() {
3b1c37fe 213 $relType = CRM_Utils_Request::retrieve('rel_type', 'String', CRM_Core_DAO::$_nullObject, TRUE);
c91df8b4 214 $relContactID = CRM_Utils_Request::retrieve('rel_contact', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
a3d827a7
CW
215 $originalCid = CRM_Utils_Request::retrieve('cid', 'Positive');
216 $relationshipID = CRM_Utils_Request::retrieve('rel_id', 'Positive');
c91df8b4 217 $caseID = CRM_Utils_Request::retrieve('case_id', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
6a488035 218
3b1c37fe
CW
219 if (!CRM_Case_BAO_Case::accessCase($caseID)) {
220 CRM_Utils_System::permissionDenied();
221 }
6a488035 222
be2fb01f 223 $ret = ['is_error' => 0];
c91df8b4 224
3b1c37fe 225 list($relTypeId, $b, $a) = explode('_', $relType);
14a679f1 226
3b1c37fe
CW
227 if ($relationshipID && $originalCid) {
228 CRM_Case_BAO_Case::endCaseRole($caseID, $a, $originalCid, $relTypeId);
229 }
14a679f1 230
3b1c37fe 231 $clientList = CRM_Case_BAO_Case::getCaseClients($caseID);
14a679f1 232
3b1c37fe
CW
233 // Loop through multiple case clients
234 foreach ($clientList as $i => $sourceContactID) {
bb76ee5a 235 try {
a0fdfb65 236 $params = [
bb76ee5a
FG
237 'case_id' => $caseID,
238 'relationship_type_id' => $relTypeId,
239 "contact_id_$a" => $relContactID,
240 "contact_id_$b" => $sourceContactID,
a0fdfb65
MD
241 'sequential' => TRUE,
242 ];
243 // first check if there is any existing relationship present with same parameters.
244 // If yes then update the relationship by setting active and start date to current time
245 $relationship = civicrm_api3('Relationship', 'get', $params)['values'];
246 $params = array_merge(CRM_Utils_Array::value(0, $relationship, $params), [
bb76ee5a 247 'start_date' => 'now',
a0fdfb65
MD
248 'is_active' => TRUE,
249 'end_date' => '',
250 ]);
251 $result = civicrm_api3('relationship', 'create', $params);
bb76ee5a
FG
252 }
253 catch (CiviCRM_API3_Exception $e) {
254 $ret['is_error'] = 1;
255 $ret['error_message'] = $e->getMessage();
256 }
3b1c37fe
CW
257 // Save activity only for the primary (first) client
258 if ($i == 0 && empty($result['is_error'])) {
3e4eb9aa 259 CRM_Case_BAO_Case::createCaseRoleActivity($caseID, $result['id'], $relContactID, $sourceContactID);
c91df8b4 260 }
6a488035 261 }
3e4eb9aa
JP
262 if (!empty($_REQUEST['is_unit_test'])) {
263 return $ret;
264 }
6a488035 265
ecdef330 266 CRM_Utils_JSON::output($ret);
6a488035
TO
267 }
268
269 /**
fe482240 270 * Fetch the custom field help.
6a488035 271 */
00be9182 272 public static function customField() {
353ffa53 273 $fieldId = CRM_Utils_Type::escape($_REQUEST['id'], 'Integer');
be2fb01f
CW
274 $params = ['id' => $fieldId];
275 $returnProperties = ['help_pre', 'help_post'];
276 $values = [];
6a488035
TO
277
278 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_CustomField', $params, $values, $returnProperties);
ecdef330 279 CRM_Utils_JSON::output($values);
6a488035
TO
280 }
281
00be9182 282 public static function groupTree() {
d42a224c 283 CRM_Utils_System::setHttpHeader('Content-Type', 'application/json');
6a488035
TO
284 $gids = CRM_Utils_Type::escape($_GET['gids'], 'String');
285 echo CRM_Contact_BAO_GroupNestingCache::json($gids);
286 CRM_Utils_System::civiExit();
287 }
288
6a488035 289 /**
fe482240 290 * Delete custom value.
6a488035 291 */
00be9182 292 public static function deleteCustomValue() {
d42a224c 293 CRM_Utils_System::setHttpHeader('Content-Type', 'text/plain');
6a488035
TO
294 $customValueID = CRM_Utils_Type::escape($_REQUEST['valueID'], 'Positive');
295 $customGroupID = CRM_Utils_Type::escape($_REQUEST['groupID'], 'Positive');
a3d827a7 296 $contactId = CRM_Utils_Request::retrieve('contactId', 'Positive');
6a488035 297 CRM_Core_BAO_CustomValue::deleteCustomValue($customValueID, $customGroupID);
a4cce21a 298 if ($contactId) {
7fa9167d 299 echo CRM_Contact_BAO_Contact::getCountComponent('custom_' . $customGroupID, $contactId);
6a488035
TO
300 }
301
2b68a50c 302 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
6a488035
TO
303 CRM_Utils_System::civiExit();
304 }
305
6a488035 306 /**
fe482240 307 * check the CMS username.
ce80b209 308 */
69078420 309 public static function checkUserName() {
be2fb01f 310 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), ['for', 'ts']);
a3d827a7
CW
311 $sig = CRM_Utils_Request::retrieve('sig', 'String');
312 $for = CRM_Utils_Request::retrieve('for', 'String');
272081ca
TO
313 if (
314 CRM_Utils_Time::getTimeRaw() > $_REQUEST['ts'] + self::CHECK_USERNAME_TTL
7fa9167d 315 || $for != 'civicrm/ajax/cmsuser'
316 || !$signer->validate($sig, $_REQUEST)
272081ca 317 ) {
be2fb01f 318 $user = ['name' => 'error'];
8be1a839 319 CRM_Utils_JSON::output($user);
272081ca
TO
320 }
321
6a488035 322 $config = CRM_Core_Config::singleton();
4c68cf7b 323 $username = trim(CRM_Utils_Array::value('cms_name', $_REQUEST));
6a488035 324
be2fb01f 325 $params = ['name' => $username];
6a488035 326
be2fb01f 327 $errors = [];
6a488035
TO
328 $config->userSystem->checkUserNameEmailExists($params, $errors);
329
330 if (isset($errors['cms_name']) || isset($errors['name'])) {
b44e3f84 331 //user name is not available
be2fb01f 332 $user = ['name' => 'no'];
8be1a839 333 CRM_Utils_JSON::output($user);
6a488035
TO
334 }
335 else {
336 //user name is available
be2fb01f 337 $user = ['name' => 'yes'];
8be1a839 338 CRM_Utils_JSON::output($user);
6a488035 339 }
8be1a839
TO
340
341 // Not reachable: JSON::output() above exits.
6a488035
TO
342 CRM_Utils_System::civiExit();
343 }
344
345 /**
fe482240 346 * Function to get email address of a contact.
6a488035 347 */
00be9182 348 public static function getContactEmail() {
3c0dc533 349 $queryStrings = [];
9c1bc317 350 $name = $_GET['name'] ?? NULL;
3c0dc533 351 if ($name) {
352 $name = CRM_Utils_Type::escape($name, 'String');
353 $wildCard = Civi::settings()->get('includeWildCardInName') ? '%' : '';
354 foreach (['cc.sort_name', 'ce.email'] as $column) {
355 $queryStrings[] = "{$column} LIKE '{$wildCard}{$name}%'";
8c0ea1d7 356 }
3c0dc533 357 $result = [];
358 $rowCount = Civi::settings()->get('search_autocomplete_count');
effdc2d9 359
3c0dc533 360 // add acl clause here
361 list($aclFrom, $aclWhere) = CRM_Contact_BAO_Contact_Permission::cacheClause('cc');
362 if ($aclWhere) {
363 $aclWhere = "AND {$aclWhere}";
6a488035 364 }
3c0dc533 365 foreach ($queryStrings as &$queryString) {
366 $queryString = "(
6a488035
TO
367SELECT sort_name name, ce.email, cc.id
368FROM civicrm_email ce INNER JOIN civicrm_contact cc ON cc.id = ce.contact_id
369 {$aclFrom}
370WHERE ce.on_hold = 0 AND cc.is_deceased = 0 AND cc.do_not_email = 0 AND {$queryString}
371 {$aclWhere}
3c0dc533 372LIMIT {$rowCount}
373)";
374 }
375 $query = implode(' UNION ', $queryStrings) . " LIMIT {$rowCount}";
376
377 // send query to hook to be modified if needed
378 CRM_Utils_Hook::contactListQuery($query,
379 $name,
380 CRM_Utils_Request::retrieve('context', 'Alphanumeric'),
381 CRM_Utils_Request::retrieve('cid', 'Positive')
382 );
383
384 $dao = CRM_Core_DAO::executeQuery($query);
385
386 while ($dao->fetch()) {
387 //working here
388 $result[] = [
389 'text' => '"' . $dao->name . '" <' . $dao->email . '>',
390 'id' => (CRM_Utils_Array::value('id', $_GET)) ? "{$dao->id}::{$dao->email}" : '"' . $dao->name . '" <' . $dao->email . '>',
391 ];
6a488035 392 }
3c0dc533 393 CRM_Utils_JSON::output($result);
6a488035
TO
394 }
395 CRM_Utils_System::civiExit();
396 }
397
00be9182 398 public static function getContactPhone() {
6a488035
TO
399
400 $queryString = NULL;
3ae9c7d6 401 $sqlParmas = [];
6a488035
TO
402 //check for mobile type
403 $phoneTypes = CRM_Core_OptionGroup::values('phone_type', TRUE, FALSE, FALSE, NULL, 'name');
9c1bc317 404 $mobileType = $phoneTypes['Mobile'] ?? NULL;
6a488035 405
3ae9c7d6 406 $name = CRM_Utils_Request::retrieveValue('name', 'String', NULL, FALSE, 'GET');
a4cce21a 407 if ($name) {
3ae9c7d6
SL
408 $key = (int) count(array_keys($sqlParmas)) + 1;
409 $queryString = " ( cc.sort_name LIKE %{$key} OR cp.phone LIKE %{$key} ) ";
410 $sqlParams[$key] = ['%' . $name . '%', 'String'];
6a488035 411 }
a4cce21a 412 else {
3ae9c7d6 413 $cid = CRM_Utils_Request::retrieveValue('cid', 'CommaSeparatedIntegers', NULL, FALSE, 'GET');
d75f2f47 414 if ($cid) {
a4cce21a 415 $queryString = " cc.id IN ( $cid )";
6a488035 416 }
6a488035
TO
417 }
418
419 if ($queryString) {
be2fb01f 420 $result = [];
3ae9c7d6
SL
421 $offset = (int) CRM_Utils_Request::retrieveValue('offset', 'Integer', 0, FALSE, 'GET');
422 $rowCount = (int) CRM_Utils_Request::retrieveValue('rowcount', 'Integer', 20, FALSE, 'GET');
bf00d1b6 423
6a488035
TO
424 // add acl clause here
425 list($aclFrom, $aclWhere) = CRM_Contact_BAO_Contact_Permission::cacheClause('cc');
426 if ($aclWhere) {
427 $aclWhere = " AND $aclWhere";
428 }
429
430 $query = "
431SELECT sort_name name, cp.phone, cc.id
432FROM civicrm_phone cp INNER JOIN civicrm_contact cc ON cc.id = cp.contact_id
433 {$aclFrom}
434WHERE cc.is_deceased = 0 AND cc.do_not_sms = 0 AND cp.phone_type_id = {$mobileType} AND {$queryString}
435 {$aclWhere}
436LIMIT {$offset}, {$rowCount}
437";
438
439 // send query to hook to be modified if needed
440 CRM_Utils_Hook::contactListQuery($query,
441 $name,
edc80cda 442 CRM_Utils_Request::retrieve('context', 'Alphanumeric'),
a3d827a7 443 CRM_Utils_Request::retrieve('cid', 'Positive')
6a488035
TO
444 );
445
3ae9c7d6 446 $dao = CRM_Core_DAO::executeQuery($query, $sqlParams);
6a488035
TO
447
448 while ($dao->fetch()) {
be2fb01f 449 $result[] = [
b792e485 450 'text' => '"' . $dao->name . '" (' . $dao->phone . ')',
6a488035 451 'id' => (CRM_Utils_Array::value('id', $_GET)) ? "{$dao->id}::{$dao->phone}" : '"' . $dao->name . '" <' . $dao->phone . '>',
be2fb01f 452 ];
6a488035 453 }
8be1a839 454 CRM_Utils_JSON::output($result);
6a488035
TO
455 }
456 CRM_Utils_System::civiExit();
457 }
458
00be9182 459 public static function buildSubTypes() {
a3d827a7 460 $parent = CRM_Utils_Request::retrieve('parentId', 'Positive');
6a488035
TO
461
462 switch ($parent) {
463 case 1:
464 $contactType = 'Individual';
465 break;
466
467 case 2:
468 $contactType = 'Household';
469 break;
470
471 case 4:
472 $contactType = 'Organization';
473 break;
474 }
475
476 $subTypes = CRM_Contact_BAO_ContactType::subTypePairs($contactType, FALSE, NULL);
477 asort($subTypes);
ecdef330 478 CRM_Utils_JSON::output($subTypes);
6a488035
TO
479 }
480
00be9182 481 public static function buildDedupeRules() {
a3d827a7 482 $parent = CRM_Utils_Request::retrieve('parentId', 'Positive');
6a488035
TO
483
484 switch ($parent) {
485 case 1:
486 $contactType = 'Individual';
487 break;
488
489 case 2:
490 $contactType = 'Household';
491 break;
492
493 case 4:
494 $contactType = 'Organization';
495 break;
496 }
497
498 $dedupeRules = CRM_Dedupe_BAO_RuleGroup::getByType($contactType);
499
ecdef330 500 CRM_Utils_JSON::output($dedupeRules);
6a488035
TO
501 }
502
503 /**
fe482240 504 * Function used for CiviCRM dashboard operations.
6a488035 505 */
00be9182 506 public static function dashboard() {
ce2cc43e 507 switch ($_REQUEST['op']) {
6a488035 508 case 'save_columns':
26d9b052 509 CRM_Core_BAO_Dashboard::saveDashletChanges($_REQUEST['columns'] ?? NULL);
ce2cc43e
CW
510 break;
511
6a488035
TO
512 case 'delete_dashlet':
513 $dashletID = CRM_Utils_Type::escape($_REQUEST['dashlet_id'], 'Positive');
a279e546 514 CRM_Core_DAO_Dashboard::deleteRecord(['id' => $dashletID]);
6a488035
TO
515 }
516
ce2cc43e 517 CRM_Utils_System::civiExit();
6a488035
TO
518 }
519
520 /**
fe482240 521 * Retrieve signature based on email id.
6a488035 522 */
00be9182 523 public static function getSignature() {
6a488035 524 $emailID = CRM_Utils_Type::escape($_REQUEST['emailID'], 'Positive');
353ffa53
TO
525 $query = "SELECT signature_text, signature_html FROM civicrm_email WHERE id = {$emailID}";
526 $dao = CRM_Core_DAO::executeQuery($query);
6a488035 527
be2fb01f 528 $signatures = [];
6a488035 529 while ($dao->fetch()) {
be2fb01f 530 $signatures = [
6a488035
TO
531 'signature_text' => $dao->signature_text,
532 'signature_html' => $dao->signature_html,
be2fb01f 533 ];
6a488035
TO
534 }
535
ecdef330 536 CRM_Utils_JSON::output($signatures);
6a488035
TO
537 }
538
539 /**
100fef9d 540 * Process dupes.
6a488035 541 */
00be9182 542 public static function processDupes() {
6a488035 543 $oper = CRM_Utils_Type::escape($_REQUEST['op'], 'String');
353ffa53
TO
544 $cid = CRM_Utils_Type::escape($_REQUEST['cid'], 'Positive');
545 $oid = CRM_Utils_Type::escape($_REQUEST['oid'], 'Positive');
6a488035
TO
546
547 if (!$oper || !$cid || !$oid) {
548 return;
549 }
550
2d1fefa0 551 $status = self::markNonDuplicates($cid, $oid, $oper);
6a488035 552
be2fb01f 553 CRM_Utils_JSON::output(['status' => ($status) ? $oper : $status]);
6a488035
TO
554 }
555
183ec330 556 /**
557 * Retrieve list of duplicate pairs from cache table.
558 */
00be9182 559 public static function getDedupes() {
63ef778e 560 $offset = isset($_REQUEST['start']) ? CRM_Utils_Type::escape($_REQUEST['start'], 'Integer') : 0;
561 $rowCount = isset($_REQUEST['length']) ? CRM_Utils_Type::escape($_REQUEST['length'], 'Integer') : 25;
6a488035 562
dc6285d5 563 $gid = CRM_Utils_Request::retrieve('gid', 'Positive');
564 $rgid = CRM_Utils_Request::retrieve('rgid', 'Positive');
997a03fe 565 $limit = CRM_Utils_Request::retrieveValue('limit', 'Positive', 0);
9f54f049 566 $null = NULL;
567 $criteria = CRM_Utils_Request::retrieve('criteria', 'Json', $null, FALSE, '{}');
bb22928b 568 $selected = CRM_Utils_Request::retrieveValue('selected', 'Boolean');
63ef778e 569 if ($rowCount < 0) {
570 $rowCount = 0;
571 }
6a488035 572
1f51ef4e 573 $whereClause = $orderByClause = '';
997a03fe 574 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($rgid, $gid, json_decode($criteria, TRUE), TRUE, $limit);
9f54f049 575
69078420 576 $searchRows = [];
6a488035 577
ed92673b 578 $searchParams = self::getSearchOptionsFromRequest();
be2fb01f 579 $queryParams = [];
ed92673b 580
63ef778e 581 $join = '';
be2fb01f 582 $where = [];
579ea9bc 583
ed92673b 584 $isOrQuery = self::isOrQuery();
585
586 $nextParamKey = 3;
be2fb01f 587 $mappings = [
e67dcaf8 588 'dst' => 'cc1.display_name',
589 'src' => 'cc2.display_name',
590 'dst_email' => 'ce1.email',
591 'src_email' => 'ce2.email',
592 'dst_postcode' => 'ca1.postal_code',
593 'src_postcode' => 'ca2.postal_code',
594 'dst_street' => 'ca1.street',
595 'src_street' => 'ca2.street',
be2fb01f 596 ];
ed92673b 597
598 foreach ($mappings as $key => $dbName) {
599 if (!empty($searchParams[$key])) {
12af7add 600 // CRM-18694.
601 $wildcard = strstr($key, 'postcode') ? '' : '%';
be2fb01f 602 $queryParams[$nextParamKey] = [$wildcard . $searchParams[$key] . '%', 'String'];
ed92673b 603 $where[] = $dbName . " LIKE %{$nextParamKey} ";
604 $nextParamKey++;
605 }
63ef778e 606 }
ed92673b 607
608 if ($isOrQuery) {
69078420 609 $whereClause = ' ( ' . implode(' OR ', $where) . ' ) ';
63ef778e 610 }
611 else {
612 if (!empty($where)) {
69078420 613 $whereClause = implode(' AND ', $where);
63ef778e 614 }
f931b74c 615 }
63ef778e 616 $whereClause .= $whereClause ? ' AND de.id IS NULL' : ' de.id IS NULL';
6a488035 617
63ef778e 618 if ($selected) {
619 $whereClause .= ' AND pn.is_selected = 1';
620 }
2988f5c7 621 $join .= CRM_Dedupe_Merger::getJoinOnDedupeTable();
63ef778e 622
be2fb01f 623 $select = [
e67dcaf8 624 'cc1.contact_type' => 'dst_contact_type',
625 'cc1.display_name' => 'dst_display_name',
626 'cc1.contact_sub_type' => 'dst_contact_sub_type',
627 'cc2.contact_type' => 'src_contact_type',
628 'cc2.display_name' => 'src_display_name',
629 'cc2.contact_sub_type' => 'src_contact_sub_type',
630 'ce1.email' => 'dst_email',
631 'ce2.email' => 'src_email',
632 'ca1.postal_code' => 'dst_postcode',
633 'ca2.postal_code' => 'src_postcode',
634 'ca1.street_address' => 'dst_street',
635 'ca2.street_address' => 'src_street',
be2fb01f 636 ];
63ef778e 637
f931b74c 638 if ($select) {
63ef778e 639 $join .= " INNER JOIN civicrm_contact cc1 ON cc1.id = pn.entity_id1";
640 $join .= " INNER JOIN civicrm_contact cc2 ON cc2.id = pn.entity_id2";
641 $join .= " LEFT JOIN civicrm_email ce1 ON (ce1.contact_id = pn.entity_id1 AND ce1.is_primary = 1 )";
642 $join .= " LEFT JOIN civicrm_email ce2 ON (ce2.contact_id = pn.entity_id2 AND ce2.is_primary = 1 )";
643 $join .= " LEFT JOIN civicrm_address ca1 ON (ca1.contact_id = pn.entity_id1 AND ca1.is_primary = 1 )";
644 $join .= " LEFT JOIN civicrm_address ca2 ON (ca2.contact_id = pn.entity_id2 AND ca2.is_primary = 1 )";
645 }
ed92673b 646 $iTotal = CRM_Core_BAO_PrevNextCache::getCount($cacheKeyString, $join, $whereClause, '=', $queryParams);
1f51ef4e 647 if (!empty($_REQUEST['order'])) {
648 foreach ($_REQUEST['order'] as $orderInfo) {
649 if (!empty($orderInfo['column'])) {
650 $orderColumnNumber = $orderInfo['column'];
2c032aca 651 $dir = CRM_Utils_Type::escape($orderInfo['dir'], 'MysqlOrderByDirection', FALSE);
1f51ef4e 652 }
63ef778e 653 }
9c1bc317 654 $columnDetails = $_REQUEST['columns'][$orderColumnNumber] ?? NULL;
63ef778e 655 }
f931b74c 656 if (!empty($columnDetails)) {
63ef778e 657 switch ($columnDetails['data']) {
f931b74c 658 case 'src':
e67dcaf8 659 $orderByClause = " ORDER BY cc2.display_name {$dir}";
f931b74c 660 break;
661
662 case 'src_email':
e67dcaf8 663 $orderByClause = " ORDER BY ce2.email {$dir}";
f931b74c 664 break;
665
666 case 'src_street':
e67dcaf8 667 $orderByClause = " ORDER BY ca2.street_address {$dir}";
f931b74c 668 break;
669
670 case 'src_postcode':
e67dcaf8 671 $orderByClause = " ORDER BY ca2.postal_code {$dir}";
f931b74c 672 break;
673
674 case 'dst':
e67dcaf8 675 $orderByClause = " ORDER BY cc1.display_name {$dir}";
f931b74c 676 break;
677
678 case 'dst_email':
e67dcaf8 679 $orderByClause = " ORDER BY ce1.email {$dir}";
f931b74c 680 break;
681
682 case 'dst_street':
e67dcaf8 683 $orderByClause = " ORDER BY ca1.street_address {$dir}";
f931b74c 684 break;
685
686 case 'dst_postcode':
e67dcaf8 687 $orderByClause = " ORDER BY ca1.postal_code {$dir}";
f931b74c 688 break;
689
690 default:
063ffcb7 691 $orderByClause = " ORDER BY cc1.display_name ASC";
f931b74c 692 break;
63ef778e 693 }
694 }
6a488035 695
ed92673b 696 $dupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, $join, $whereClause, $offset, $rowCount, $select, $orderByClause, TRUE, $queryParams);
63ef778e 697 $iFilteredTotal = CRM_Core_DAO::singleValueQuery("SELECT FOUND_ROWS()");
6a488035 698
63ef778e 699 $count = 0;
700 foreach ($dupePairs as $key => $pairInfo) {
e67dcaf8 701 $pair = $pairInfo['data'];
9c1bc317
CW
702 $srcContactSubType = $pairInfo['src_contact_sub_type'] ?? NULL;
703 $dstContactSubType = $pairInfo['dst_contact_sub_type'] ?? NULL;
63ef778e 704 $srcTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($srcContactSubType ?
705 $srcContactSubType : $pairInfo['src_contact_type'],
706 FALSE,
e67dcaf8 707 $pairInfo['entity_id2']
63ef778e 708 );
709 $dstTypeImage = CRM_Contact_BAO_Contact_Utils::getImage($dstContactSubType ?
710 $dstContactSubType : $pairInfo['dst_contact_type'],
711 FALSE,
e67dcaf8 712 $pairInfo['entity_id1']
63ef778e 713 );
6a488035 714
63ef778e 715 $searchRows[$count]['is_selected'] = $pairInfo['is_selected'];
716 $searchRows[$count]['is_selected_input'] = "<input type='checkbox' class='crm-dedupe-select' name='pnid_{$pairInfo['prevnext_id']}' value='{$pairInfo['is_selected']}' onclick='toggleDedupeSelect(this)'>";
717 $searchRows[$count]['src_image'] = $srcTypeImage;
e67dcaf8 718 $searchRows[$count]['src'] = CRM_Utils_System::href($pair['srcName'], 'civicrm/contact/view', "reset=1&cid={$pairInfo['entity_id2']}");
9c1bc317
CW
719 $searchRows[$count]['src_email'] = $pairInfo['src_email'] ?? NULL;
720 $searchRows[$count]['src_street'] = $pairInfo['src_street'] ?? NULL;
721 $searchRows[$count]['src_postcode'] = $pairInfo['src_postcode'] ?? NULL;
63ef778e 722 $searchRows[$count]['dst_image'] = $dstTypeImage;
e67dcaf8 723 $searchRows[$count]['dst'] = CRM_Utils_System::href($pair['dstName'], 'civicrm/contact/view', "reset=1&cid={$pairInfo['entity_id1']}");
9c1bc317
CW
724 $searchRows[$count]['dst_email'] = $pairInfo['dst_email'] ?? NULL;
725 $searchRows[$count]['dst_street'] = $pairInfo['dst_street'] ?? NULL;
726 $searchRows[$count]['dst_postcode'] = $pairInfo['dst_postcode'] ?? NULL;
da3afd4a 727 $searchRows[$count]['conflicts'] = str_replace("',", "',<br/>", CRM_Utils_Array::value('conflicts', $pair));
9c1bc317 728 $searchRows[$count]['weight'] = $pair['weight'] ?? NULL;
63ef778e 729
fd630ef9 730 if (!empty($pairInfo['data']['canMerge'])) {
c0cc2ad4 731 $mergeParams = [
732 'reset' => 1,
69078420
SL
733 'cid' => $pairInfo['entity_id1'],
734 'oid' => $pairInfo['entity_id2'],
735 'action' => 'update',
736 'rgid' => $rgid,
737 'criteria' => $criteria,
738 'limit' => CRM_Utils_Request::retrieve('limit', 'Integer'),
739 ];
6a488035 740 if ($gid) {
c0cc2ad4 741 $mergeParams['gid'] = $gid;
6a488035
TO
742 }
743
fd630ef9 744 $searchRows[$count]['actions'] = "<a class='crm-dedupe-flip' href='#' data-pnid={$pairInfo['prevnext_id']}>" . ts('flip') . "</a>&nbsp;|&nbsp;";
da3afd4a 745 $searchRows[$count]['actions'] .= CRM_Utils_System::href(ts('merge'), 'civicrm/contact/merge', $mergeParams);
fd630ef9 746 $searchRows[$count]['actions'] .= "&nbsp;|&nbsp;<a id='notDuplicate' href='#' onClick=\"processDupes( {$pairInfo['entity_id1']}, {$pairInfo['entity_id2']}, 'dupe-nondupe', 'dupe-listing'); return false;\">" . ts('not a duplicate') . "</a>";
6a488035
TO
747 }
748 else {
63ef778e 749 $searchRows[$count]['actions'] = '<em>' . ts('Insufficient access rights - cannot merge') . '</em>';
6a488035 750 }
63ef778e 751 $count++;
6a488035
TO
752 }
753
be2fb01f 754 $dupePairs = [
22b232f3 755 'data' => $searchRows,
756 'recordsTotal' => $iTotal,
757 'recordsFiltered' => $iFilteredTotal,
be2fb01f 758 ];
1f51ef4e 759 if (!empty($_REQUEST['is_unit_test'])) {
760 return $dupePairs;
761 }
22b232f3 762 CRM_Utils_JSON::output($dupePairs);
6a488035
TO
763 }
764
ed92673b 765 /**
766 * Get the searchable options from the request.
767 *
768 * @return array
769 */
770 public static function getSearchOptionsFromRequest() {
be2fb01f 771 $searchParams = [];
9c1bc317 772 $searchData = $_REQUEST['search'] ?? NULL;
ed92673b 773 $searchData['value'] = CRM_Utils_Type::escape($searchData['value'], 'String');
be2fb01f 774 $selectorElements = [
ed92673b 775 'is_selected',
776 'is_selected_input',
777 'src_image',
778 'src',
779 'src_email',
780 'src_street',
781 'src_postcode',
782 'dst_image',
783 'dst',
784 'dst_email',
785 'dst_street',
786 'dst_postcode',
787 'conflicts',
788 'weight',
789 'actions',
be2fb01f 790 ];
ed92673b 791 $columns = $_REQUEST['columns'];
792
793 foreach ($columns as $column) {
794 if (!empty($column['search']['value']) && in_array($column['data'], $selectorElements)) {
795 $searchParams[$column['data']] = CRM_Utils_Type::escape($column['search']['value'], 'String');
796 }
797 elseif (!empty($searchData['value'])) {
798 $searchParams[$column['data']] = $searchData['value'];
799 }
800 }
801 return $searchParams;
802 }
803
804 /**
805 * Is the query an OR query.
806 *
807 * If a generic search value is passed in - ie. $_REQUEST['search']['value'] = 'abc'
808 * then all fields are searched for this.
809 *
810 * It is unclear if there is any code that still passes this in or whether is is just legacy. It
811 * could cause a server-killing query on a large site so it probably is NOT in use if we haven't
812 * had complaints.
813 *
814 * @return bool
815 */
816 public static function isOrQuery() {
9c1bc317 817 $searchData = $_REQUEST['search'] ?? NULL;
ed92673b 818 return !empty($searchData['value']);
819 }
820
2d1fefa0 821 /**
822 * Mark not duplicates.
823 *
824 * Note this function would sensibly be replaced by an api-call but extracting here to add a test first.
825 *
826 * I would have like to make it private but test class accesses it & it doesn't warrant being a BAO class
827 * as it should feel very endangered.
828 *
829 * @param int $cid
830 * @param int $oid
041ecc95 831 * @param "dupe-nondupe|nondupe-dupe" $oper
2d1fefa0 832 *
833 * @return \CRM_Core_DAO|mixed|null
834 */
835 public static function markNonDuplicates($cid, $oid, $oper) {
ac2751b3 836 if ($oper == 'dupe-nondupe') {
837 try {
838 civicrm_api3('Exception', 'create', ['contact_id1' => $cid, 'contact_id2' => $oid]);
839 return TRUE;
840 }
841 catch (CiviCRM_API3_Exception $e) {
842 return FALSE;
843 }
844 }
845
2d1fefa0 846 $exception = new CRM_Dedupe_DAO_Exception();
847 $exception->contact_id1 = $cid;
848 $exception->contact_id2 = $oid;
849 //make sure contact2 > contact1.
850 if ($cid > $oid) {
851 $exception->contact_id1 = $oid;
852 $exception->contact_id2 = $cid;
853 }
854 $exception->find(TRUE);
855 $status = NULL;
ac2751b3 856
2d1fefa0 857 if ($oper == 'nondupe-dupe') {
858 $status = $exception->delete();
859 }
860 return $status;
861 }
862
6a488035 863 /**
fe482240 864 * Retrieve a PDF Page Format for the PDF Letter form.
6a488035 865 */
00be9182 866 public function pdfFormat() {
6a488035
TO
867 $formatId = CRM_Utils_Type::escape($_REQUEST['formatId'], 'Integer');
868
869 $pdfFormat = CRM_Core_BAO_PdfFormat::getById($formatId);
870
ecdef330 871 CRM_Utils_JSON::output($pdfFormat);
6a488035
TO
872 }
873
874 /**
fe482240 875 * Retrieve Paper Size dimensions.
6a488035 876 */
00be9182 877 public static function paperSize() {
6a488035
TO
878 $paperSizeName = CRM_Utils_Type::escape($_REQUEST['paperSizeName'], 'String');
879
880 $paperSize = CRM_Core_BAO_PaperSize::getByName($paperSizeName);
881
ecdef330 882 CRM_Utils_JSON::output($paperSize);
6a488035
TO
883 }
884
183ec330 885 /**
886 * Swap contacts in a dupe pair i.e main with duplicate contact.
ea3ddccf 887 *
888 * @param int $prevNextId
183ec330 889 */
f931b74c 890 public static function flipDupePairs($prevNextId = NULL) {
fd630ef9 891 if (!$prevNextId) {
808c05a9 892 // @todo figure out if this is always POST & specify that rather than inexact GET
6d13d1c4
ML
893
894 // We cannot use CRM_Utils_Request::retrieve() because it might be an array.
895 // It later gets validated in escapeAll below.
896 $prevNextId = $_REQUEST['pnid'];
fd630ef9 897 }
808c05a9 898
899 $onlySelected = FALSE;
fd630ef9 900 if (is_array($prevNextId) && !CRM_Utils_Array::crmIsEmptyArray($prevNextId)) {
808c05a9 901 $onlySelected = TRUE;
fd630ef9 902 }
808c05a9 903 $prevNextId = CRM_Utils_Type::escapeAll((array) $prevNextId, 'Positive');
904 CRM_Core_BAO_PrevNextCache::flipPair($prevNextId, $onlySelected);
558cd7c7 905 CRM_Utils_System::civiExit();
fd630ef9 906 }
907
aeb97cc1
CW
908 /**
909 * Used to store selected contacts across multiple pages in advanced search.
910 */
00be9182 911 public static function selectUnselectContacts() {
9c1bc317
CW
912 $name = $_REQUEST['name'] ?? NULL;
913 $cacheKey = $_REQUEST['qfKey'] ?? NULL;
353ffa53 914 $state = CRM_Utils_Array::value('state', $_REQUEST, 'checked');
6a488035
TO
915 $variableType = CRM_Utils_Array::value('variableType', $_REQUEST, 'single');
916
917 $actionToPerform = CRM_Utils_Array::value('action', $_REQUEST, 'select');
918
919 if ($variableType == 'multiple') {
920 // action post value only works with multiple type variable
921 if ($name) {
922 //multiple names like mark_x_1-mark_x_2 where 1,2 are cids
923 $elements = explode('-', $name);
924 foreach ($elements as $key => $element) {
925 $elements[$key] = self::_convertToId($element);
926 }
91209206 927 CRM_Utils_Type::escapeAll($elements, 'Integer');
0b8038a6 928 Civi::service('prevnext')->markSelection($cacheKey, $actionToPerform, $elements);
6a488035
TO
929 }
930 else {
0b8038a6 931 Civi::service('prevnext')->markSelection($cacheKey, $actionToPerform);
6a488035
TO
932 }
933 }
934 elseif ($variableType == 'single') {
935 $cId = self::_convertToId($name);
91209206 936 CRM_Utils_Type::escape($cId, 'Integer');
6a488035 937 $action = ($state == 'checked') ? 'select' : 'unselect';
0b8038a6 938 Civi::service('prevnext')->markSelection($cacheKey, $action, $cId);
6a488035 939 }
b7994703 940 $contactIds = Civi::service('prevnext')->getSelection($cacheKey);
6a488035
TO
941 $countSelectionCids = count($contactIds[$cacheKey]);
942
be2fb01f 943 $arrRet = ['getCount' => $countSelectionCids];
ecdef330 944 CRM_Utils_JSON::output($arrRet);
6a488035
TO
945 }
946
4319322b 947 /**
100fef9d 948 * @param string $name
4319322b
EM
949 *
950 * @return string
951 */
00be9182 952 public static function _convertToId($name) {
6a488035
TO
953 if (substr($name, 0, CRM_Core_Form::CB_PREFIX_LEN) == CRM_Core_Form::CB_PREFIX) {
954 $cId = substr($name, CRM_Core_Form::CB_PREFIX_LEN);
955 }
956 return $cId;
957 }
958
00be9182 959 public static function getAddressDisplay() {
a3d827a7 960 $contactId = CRM_Utils_Request::retrieve('contact_id', 'Positive');
6a488035
TO
961 if (!$contactId) {
962 $addressVal["error_message"] = "no contact id found";
963 }
964 else {
be2fb01f 965 $entityBlock = [
408b79bf 966 'contact_id' => $contactId,
967 'entity_id' => $contactId,
be2fb01f 968 ];
6a488035
TO
969 $addressVal = CRM_Core_BAO_Address::getValues($entityBlock);
970 }
971
ecdef330 972 CRM_Utils_JSON::output($addressVal);
6a488035 973 }
40458f6c 974
183ec330 975 /**
976 * Mark dupe pairs as selected from un-selected state or vice-versa, in dupe cache table.
977 */
f931b74c 978 public static function toggleDedupeSelect() {
63ef778e 979 $pnid = $_REQUEST['pnid'];
980 $isSelected = CRM_Utils_Type::escape($_REQUEST['is_selected'], 'Boolean');
9d2f6d53 981 $cacheKeyString = CRM_Utils_Request::retrieve('cacheKey', 'Alphanumeric', $null, FALSE);
63ef778e 982
be2fb01f
CW
983 $params = [
984 1 => [$isSelected, 'Boolean'],
69078420
SL
985 // using % to address rows with conflicts as well
986 3 => ["$cacheKeyString%", 'String'],
be2fb01f 987 ];
63ef778e 988
989 //check pnid is_array or integer
990 $whereClause = NULL;
991 if (is_array($pnid) && !CRM_Utils_Array::crmIsEmptyArray($pnid)) {
13c42e60 992 CRM_Utils_Type::escapeAll($pnid, 'Positive');
63ef778e 993 $pnid = implode(', ', $pnid);
63ef778e 994 $whereClause = " id IN ( {$pnid} ) ";
995 }
996 else {
997 $pnid = CRM_Utils_Type::escape($pnid, 'Integer');
998 $whereClause = " id = %2";
be2fb01f 999 $params[2] = [$pnid, 'Integer'];
63ef778e 1000 }
1001
783b4b21 1002 $sql = "UPDATE civicrm_prevnext_cache SET is_selected = %1 WHERE {$whereClause} AND cachekey LIKE %3";
63ef778e 1003 CRM_Core_DAO::executeQuery($sql, $params);
1004
1005 CRM_Utils_System::civiExit();
1006 }
1007
40458f6c 1008 /**
fe482240 1009 * Retrieve contact relationships.
40458f6c 1010 */
1011 public static function getContactRelationships() {
1012 $contactID = CRM_Utils_Type::escape($_GET['cid'], 'Integer');
edc80cda 1013 $context = CRM_Utils_Request::retrieve('context', 'Alphanumeric');
7d12de7f 1014 $relationship_type_id = CRM_Utils_Type::escape(CRM_Utils_Array::value('relationship_type_id', $_GET), 'Integer', FALSE);
40458f6c 1015
b0266403
CW
1016 if (!CRM_Contact_BAO_Contact_Permission::allow($contactID)) {
1017 return CRM_Utils_System::permissionDenied();
1018 }
1019
00f11506 1020 $params = CRM_Core_Page_AJAX::defaultSortAndPagerParams();
40458f6c 1021
1022 $params['contact_id'] = $contactID;
1023 $params['context'] = $context;
4c5f31d0 1024 if ($relationship_type_id) {
cdee9432
TM
1025 $params['relationship_type_id'] = $relationship_type_id;
1026 }
40458f6c 1027
1028 // get the contact relationships
1029 $relationships = CRM_Contact_BAO_Relationship::getContactRelationshipSelector($params);
1030
7d12de7f 1031 CRM_Utils_JSON::output($relationships);
40458f6c 1032 }
96025800 1033
6a488035 1034}