Merge pull request #15192 from demeritcowboy/locate-name-or-label-2
[civicrm-core.git] / CRM / Core / BAO / PrevNextCache.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
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 +--------------------------------------------------------------------+
e70a7fc0 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
32 */
33
34/**
f47539f6 35 * BAO object for civicrm_prevnext_cache table.
6a488035
TO
36 */
37class CRM_Core_BAO_PrevNextCache extends CRM_Core_DAO_PrevNextCache {
38
b5c2afd0 39 /**
f47539f6 40 * Get the previous and next keys.
41 *
42 * @param string $cacheKey
43 * @param int $id1
44 * @param int $id2
100fef9d 45 * @param int $mergeId
f47539f6 46 * @param string $join
47 * @param string $where
b5c2afd0
EM
48 * @param bool $flip
49 *
50 * @return array
51 */
00be9182 52 public static function getPositions($cacheKey, $id1, $id2, &$mergeId = NULL, $join = NULL, $where = NULL, $flip = FALSE) {
6a488035 53 if ($flip) {
be2fb01f 54 list($id1, $id2) = [$id2, $id1];
6a488035
TO
55 }
56
57 if ($mergeId == NULL) {
58 $query = "
59SELECT id
60FROM civicrm_prevnext_cache
783b4b21 61WHERE cachekey = %3 AND
6a488035
TO
62 entity_id1 = %1 AND
63 entity_id2 = %2 AND
64 entity_table = 'civicrm_contact'
65";
66
be2fb01f
CW
67 $params = [
68 1 => [$id1, 'Integer'],
69 2 => [$id2, 'Integer'],
70 3 => [$cacheKey, 'String'],
71 ];
6a488035
TO
72
73 $mergeId = CRM_Core_DAO::singleValueQuery($query, $params);
74 }
75
be2fb01f 76 $pos = ['foundEntry' => 0];
6a488035
TO
77 if ($mergeId) {
78 $pos['foundEntry'] = 1;
79
80 if ($where) {
81
82 $where = " AND {$where}";
83
84 }
be2fb01f
CW
85 $p = [
86 1 => [$mergeId, 'Integer'],
87 2 => [$cacheKey, 'String'],
88 ];
353ffa53 89 $sql = "SELECT pn.id, pn.entity_id1, pn.entity_id2, pn.data FROM civicrm_prevnext_cache pn {$join} ";
783b4b21 90 $wherePrev = " WHERE pn.id < %1 AND pn.cachekey = %2 {$where} ORDER BY ID DESC LIMIT 1";
353ffa53 91 $sqlPrev = $sql . $wherePrev;
6a488035 92
6a488035
TO
93 $dao = CRM_Core_DAO::executeQuery($sqlPrev, $p);
94 if ($dao->fetch()) {
95 $pos['prev']['id1'] = $dao->entity_id1;
96 $pos['prev']['id2'] = $dao->entity_id2;
97 $pos['prev']['mergeId'] = $dao->id;
98 $pos['prev']['data'] = $dao->data;
99 }
100
783b4b21 101 $whereNext = " WHERE pn.id > %1 AND pn.cachekey = %2 {$where} ORDER BY ID ASC LIMIT 1";
6a488035
TO
102 $sqlNext = $sql . $whereNext;
103
104 $dao = CRM_Core_DAO::executeQuery($sqlNext, $p);
105 if ($dao->fetch()) {
106 $pos['next']['id1'] = $dao->entity_id1;
107 $pos['next']['id2'] = $dao->entity_id2;
108 $pos['next']['mergeId'] = $dao->id;
109 $pos['next']['data'] = $dao->data;
110 }
111 }
112 return $pos;
113 }
114
b5c2afd0 115 /**
f47539f6 116 * Delete an item from the prevnext cache table based on the entity.
117 *
100fef9d 118 * @param int $id
f47539f6 119 * @param string $cacheKey
b5c2afd0
EM
120 * @param string $entityTable
121 */
00be9182 122 public static function deleteItem($id = NULL, $cacheKey = NULL, $entityTable = 'civicrm_contact') {
6a488035
TO
123
124 //clear cache
125 $sql = "DELETE FROM civicrm_prevnext_cache WHERE entity_table = %1";
be2fb01f 126 $params = [1 => [$entityTable, 'String']];
6a488035
TO
127
128 if (is_numeric($id)) {
129 $sql .= " AND ( entity_id1 = %2 OR entity_id2 = %2 )";
be2fb01f 130 $params[2] = [$id, 'Integer'];
6a488035
TO
131 }
132
133 if (isset($cacheKey)) {
783b4b21 134 $sql .= " AND cachekey LIKE %3";
be2fb01f 135 $params[3] = ["{$cacheKey}%", 'String'];
6a488035
TO
136 }
137 CRM_Core_DAO::executeQuery($sql, $params);
138 }
139
b5c2afd0 140 /**
c131d228 141 * Delete pair from the previous next cache table to remove it from further merge consideration.
142 *
143 * The pair may have been flipped, so make sure we delete using both orders
f47539f6 144 *
145 * @param int $id1
146 * @param int $id2
147 * @param string $cacheKey
b5c2afd0 148 */
c131d228 149 public static function deletePair($id1, $id2, $cacheKey = NULL) {
150 $sql = "DELETE FROM civicrm_prevnext_cache WHERE entity_table = 'civicrm_contact'";
6a488035 151
c131d228 152 $pair = "(entity_id1 = %2 AND entity_id2 = %3) OR (entity_id1 = %3 AND entity_id2 = %2)";
6a488035 153 $sql .= " AND ( {$pair} )";
be2fb01f
CW
154 $params[2] = [$id1, 'Integer'];
155 $params[3] = [$id2, 'Integer'];
6a488035
TO
156
157 if (isset($cacheKey)) {
783b4b21 158 $sql .= " AND cachekey LIKE %4";
518fa0ee
SL
159 // used % to address any row with conflict-cacheKey e.g "merge Individual_8_0_conflicts"
160 $params[4] = ["{$cacheKey}%", 'String'];
6a488035
TO
161 }
162
163 CRM_Core_DAO::executeQuery($sql, $params);
164 }
165
f2ac86d1 166 /**
167 * Mark contacts as being in conflict.
168 *
169 * @param int $id1
170 * @param int $id2
171 * @param string $cacheKey
172 * @param array $conflicts
ffa59d18 173 * @param string $mode
f2ac86d1 174 *
175 * @return bool
8cec96dc 176 * @throws CRM_Core_Exception
f2ac86d1 177 */
ffa59d18 178 public static function markConflict($id1, $id2, $cacheKey, $conflicts, $mode) {
63ef778e 179 if (empty($cacheKey) || empty($conflicts)) {
180 return FALSE;
181 }
182
ad37ac8e 183 $sql = "SELECT pn.*
63ef778e 184 FROM civicrm_prevnext_cache pn
ad37ac8e 185 WHERE
186 ((pn.entity_id1 = %1 AND pn.entity_id2 = %2) OR (pn.entity_id1 = %2 AND pn.entity_id2 = %1)) AND
783b4b21 187 (cachekey = %3 OR cachekey = %4)";
be2fb01f
CW
188 $params = [
189 1 => [$id1, 'Integer'],
190 2 => [$id2, 'Integer'],
191 3 => ["{$cacheKey}", 'String'],
192 4 => ["{$cacheKey}_conflicts", 'String'],
193 ];
63ef778e 194 $pncFind = CRM_Core_DAO::executeQuery($sql, $params);
195
5214f03b 196 $conflictTexts = [];
ffa59d18 197
198 foreach ($conflicts as $entity => $entityConflicts) {
199 if ($entity === 'contact') {
200 foreach ($entityConflicts as $conflict) {
201 $conflictTexts[] = "{$conflict['title']}: '{$conflict[$id1]}' vs. '{$conflict[$id2]}'";
202 }
203 }
204 else {
205 foreach ($entityConflicts as $locationConflict) {
206 if (!is_array($locationConflict)) {
207 continue;
208 }
209 $displayField = CRM_Dedupe_Merger::getLocationBlockInfo()[$entity]['displayField'];
210 $conflictTexts[] = "{$locationConflict['title']}: '{$locationConflict[$displayField][$id1]}' vs. '{$locationConflict[$displayField][$id2]}'";
211 }
212 }
5214f03b 213 }
214 $conflictString = implode(', ', $conflictTexts);
ffa59d18 215
63ef778e 216 while ($pncFind->fetch()) {
217 $data = $pncFind->data;
218 if (!empty($data)) {
8cec96dc 219 $data = CRM_Core_DAO::unSerializeField($data, CRM_Core_DAO::SERIALIZE_PHP);
5214f03b 220 $data['conflicts'] = $conflictString;
ffa59d18 221 $data[$mode]['conflicts'] = $conflicts;
63ef778e 222
223 $pncUp = new CRM_Core_DAO_PrevNextCache();
224 $pncUp->id = $pncFind->id;
225 if ($pncUp->find(TRUE)) {
226 $pncUp->data = serialize($data);
783b4b21 227 $pncUp->cachekey = "{$cacheKey}_conflicts";
63ef778e 228 $pncUp->save();
229 }
230 }
231 }
232 return TRUE;
233 }
234
b5c2afd0 235 /**
ad37ac8e 236 * Retrieve from prev-next cache.
237 *
66eceb0b 238 * This function is used from a variety of merge related functions, although
239 * it would probably be good to converge on calling CRM_Dedupe_Merger::getDuplicatePairs.
240 *
241 * We seem to currently be storing stats in this table too & they might make more sense in
242 * the main cache table.
243 *
ad37ac8e 244 * @param string $cacheKey
245 * @param string $join
66eceb0b 246 * @param string $whereClause
b5c2afd0
EM
247 * @param int $offset
248 * @param int $rowCount
66eceb0b 249 * @param array $select
250 * @param string $orderByClause
251 * @param bool $includeConflicts
252 * Should we return rows that have already been idenfified as having a conflict.
253 * When this is TRUE you should be careful you do not set up a loop.
f47539f6 254 * @param array $params
b5c2afd0
EM
255 *
256 * @return array
257 */
be2fb01f 258 public static function retrieve($cacheKey, $join = NULL, $whereClause = NULL, $offset = 0, $rowCount = 0, $select = [], $orderByClause = '', $includeConflicts = TRUE, $params = []) {
63ef778e 259 $selectString = 'pn.*';
66eceb0b 260
f931b74c 261 if (!empty($select)) {
be2fb01f 262 $aliasArray = [];
63ef778e 263 foreach ($select as $column => $alias) {
f931b74c 264 $aliasArray[] = $column . ' as ' . $alias;
63ef778e 265 }
f931b74c 266 $selectString .= " , " . implode(' , ', $aliasArray);
63ef778e 267 }
66eceb0b 268
be2fb01f
CW
269 $params = [
270 1 => [$cacheKey, 'String'],
271 ] + $params;
6a488035 272
66eceb0b 273 if (!empty($whereClause)) {
274 $whereClause = " AND " . $whereClause;
275 }
276 if ($includeConflicts) {
783b4b21 277 $where = ' WHERE (pn.cachekey = %1 OR pn.cachekey = %2)' . $whereClause;
be2fb01f 278 $params[2] = ["{$cacheKey}_conflicts", 'String'];
66eceb0b 279 }
280 else {
783b4b21 281 $where = ' WHERE (pn.cachekey = %1)' . $whereClause;
6a488035
TO
282 }
283
66eceb0b 284 $query = "
285SELECT SQL_CALC_FOUND_ROWS {$selectString}
286FROM civicrm_prevnext_cache pn
287 {$join}
288 $where
289 $orderByClause
290";
291
6a488035 292 if ($rowCount) {
bf00d1b6
DL
293 $offset = CRM_Utils_Type::escape($offset, 'Int');
294 $rowCount = CRM_Utils_Type::escape($rowCount, 'Int');
295
6a488035
TO
296 $query .= " LIMIT {$offset}, {$rowCount}";
297 }
298
299 $dao = CRM_Core_DAO::executeQuery($query, $params);
300
be2fb01f 301 $main = [];
63ef778e 302 $count = 0;
6a488035
TO
303 while ($dao->fetch()) {
304 if (self::is_serialized($dao->data)) {
63ef778e 305 $main[$count] = unserialize($dao->data);
6a488035
TO
306 }
307 else {
63ef778e 308 $main[$count] = $dao->data;
6a488035 309 }
63ef778e 310
311 if (!empty($select)) {
be2fb01f 312 $extraData = [];
bf588234 313 foreach ($select as $sfield) {
518fa0ee 314 $extraData[$sfield] = $dao->$sfield;
63ef778e 315 }
be2fb01f 316 $main[$count] = [
f931b74c 317 'prevnext_id' => $dao->id,
318 'is_selected' => $dao->is_selected,
319 'entity_id1' => $dao->entity_id1,
320 'entity_id2' => $dao->entity_id2,
63ef778e 321 'data' => $main[$count],
be2fb01f 322 ];
63ef778e 323 $main[$count] = array_merge($main[$count], $extraData);
6a488035 324 }
63ef778e 325 $count++;
6a488035
TO
326 }
327
328 return $main;
329 }
330
b5c2afd0
EM
331 /**
332 * @param $string
333 *
334 * @return bool
335 */
6a488035 336 public static function is_serialized($string) {
ab8a593e 337 return (@unserialize($string) !== FALSE);
6a488035
TO
338 }
339
b5c2afd0 340 /**
e1c519d7
SL
341 * @param string $sqlValues string of SQLValues to insert
342 * @return array
b5c2afd0 343 */
e1c519d7
SL
344 public static function convertSetItemValues($sqlValues) {
345 $closingBrace = strpos($sqlValues, ')') - strlen($sqlValues);
346 $valueArray = array_map('trim', explode(', ', substr($sqlValues, strpos($sqlValues, '(') + 1, $closingBrace - 1)));
347 foreach ($valueArray as $key => &$value) {
348 // remove any quotes from values.
349 if (substr($value, 0, 1) == "'") {
350 $valueArray[$key] = substr($value, 1, -1);
351 }
352 }
353 return $valueArray;
354 }
6a488035 355
e1c519d7
SL
356 /**
357 * @param array|string $entity_table
358 * @param int $entity_id1
359 * @param int $entity_id2
360 * @param string $cacheKey
361 * @param string $data
362 */
363 public static function setItem($entity_table = NULL, $entity_id1 = NULL, $entity_id2 = NULL, $cacheKey = NULL, $data = NULL) {
364 // If entity table is an array we are passing in an older format where this function only had 1 param $values. We put a deprecation warning.
365 if (!empty($entity_table) && is_array($entity_table)) {
366 Civi::log()->warning('Deprecated code path. Values should not be set this is going away in the future in favour of specific function params for each column.', array('civi.tag' => 'deprecated'));
367 foreach ($values as $value) {
368 $valueArray = self::convertSetItemValues($value);
369 self::setItem($valueArray[0], $valueArray[1], $valueArray[2], $valueArray[3], $valueArray[4]);
370 }
371 }
372 else {
373 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_prevnext_cache (entity_table, entity_id1, entity_id2, cacheKey, data) VALUES
374 (%1, %2, %3, %4, '{$data}')", [
375 1 => [$entity_table, 'String'],
376 2 => [$entity_id1, 'Integer'],
377 3 => [$entity_id2, 'Integer'],
378 4 => [$cacheKey, 'String'],
379 ]);
380 }
6a488035
TO
381 }
382
b5c2afd0 383 /**
f47539f6 384 * Get count of matching rows.
385 *
386 * @param string $cacheKey
ed92673b 387 * @param string $join
388 * @param string $where
b5c2afd0 389 * @param string $op
ed92673b 390 * @param array $params
391 * Extra query params to parse into the query.
b5c2afd0
EM
392 *
393 * @return int
394 */
be2fb01f 395 public static function getCount($cacheKey, $join = NULL, $where = NULL, $op = "=", $params = []) {
6a488035
TO
396 $query = "
397SELECT COUNT(*) FROM civicrm_prevnext_cache pn
398 {$join}
783b4b21 399WHERE (pn.cachekey $op %1 OR pn.cachekey $op %2)
6a488035
TO
400";
401 if ($where) {
402 $query .= " AND {$where}";
403 }
404
be2fb01f
CW
405 $params = [
406 1 => [$cacheKey, 'String'],
407 2 => ["{$cacheKey}_conflicts", 'String'],
408 ] + $params;
64951b63 409 return (int) CRM_Core_DAO::singleValueQuery($query, $params, TRUE, FALSE);
6a488035
TO
410 }
411
b5c2afd0 412 /**
e23e26ec 413 * Repopulate the cache of merge prospects.
414 *
100fef9d
CW
415 * @param int $rgid
416 * @param int $gid
e23e26ec 417 * @param array $criteria
418 * Additional criteria to filter by.
7a9ab499 419 *
3058f4d9 420 * @param bool $checkPermissions
421 * Respect logged in user's permissions.
422 *
21a95d83 423 * @param int $searchLimit
424 * Limit for the number of contacts to be used for comparison.
425 * The search methodology finds all matches for the searchedContacts so this limits
426 * the number of searched contacts, not the matches found.
427 *
3058f4d9 428 * @throws \CRM_Core_Exception
429 * @throws \CiviCRM_API3_Exception
b5c2afd0 430 */
e1e24a57 431 public static function refillCache($rgid, $gid, $criteria, $checkPermissions, $searchLimit = 0) {
432 $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($rgid, $gid, $criteria, $checkPermissions);
6a488035
TO
433
434 // 1. Clear cache if any
783b4b21 435 $sql = "DELETE FROM civicrm_prevnext_cache WHERE cachekey LIKE %1";
be2fb01f 436 CRM_Core_DAO::executeQuery($sql, [1 => ["{$cacheKeyString}%", 'String']]);
6a488035
TO
437
438 // FIXME: we need to start using temp tables / queries here instead of arrays.
439 // And cleanup code in CRM/Contact/Page/DedupeFind.php
440
441 // 2. FILL cache
be2fb01f 442 $foundDupes = [];
6a488035 443 if ($rgid && $gid) {
21a95d83 444 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($rgid, $gid, $searchLimit);
6a488035
TO
445 }
446 elseif ($rgid) {
be2fb01f 447 $contactIDs = [];
88251439 448 // The thing we really need to filter out is any chaining that would 'DO SOMETHING' to the DB.
449 // criteria could be passed in via url so we want to ensure nothing could be in that url that
450 // would chain to a delete. Limiting to getfields for 'get' limits us to declared fields,
451 // although we might wish to revisit later to allow joins.
452 $validFieldsForRetrieval = civicrm_api3('Contact', 'getfields', ['action' => 'get'])['values'];
e23e26ec 453 if (!empty($criteria)) {
88251439 454 $contacts = civicrm_api3('Contact', 'get', array_merge([
455 'options' => ['limit' => 0],
456 'return' => 'id',
457 'check_permissions' => TRUE,
458 ], array_intersect_key($criteria['contact'], $validFieldsForRetrieval)));
e23e26ec 459 $contactIDs = array_keys($contacts['values']);
460 }
21a95d83 461 $foundDupes = CRM_Dedupe_Finder::dupes($rgid, $contactIDs, $checkPermissions, $searchLimit);
6a488035
TO
462 }
463
464 if (!empty($foundDupes)) {
1719073d 465 CRM_Dedupe_Finder::parseAndStoreDupePairs($foundDupes, $cacheKeyString);
6a488035
TO
466 }
467 }
468
00be9182 469 public static function cleanupCache() {
435fc552 470 Civi::service('prevnext')->cleanup();
6a488035
TO
471 }
472
6a488035 473 /**
fe482240 474 * Get the selections.
6a488035 475 *
b7994703
TO
476 * NOTE: This stub has been preserved because one extension in `universe`
477 * was referencing the function.
77b97be7 478 *
b7994703
TO
479 * @deprecated
480 * @see CRM_Core_PrevNextCache_Sql::getSelection()
6a488035 481 */
40ddbe99
TO
482 public static function getSelection($cacheKey, $action = 'get') {
483 return Civi::service('prevnext')->getSelection($cacheKey, $action);
6a488035
TO
484 }
485
b5c2afd0 486 /**
808c05a9 487 * Flip 2 contacts in the prevNext cache.
488 *
489 * @param array $prevNextId
490 * @param bool $onlySelected
491 * Only flip those which have been marked as selected.
492 */
493 public static function flipPair(array $prevNextId, $onlySelected) {
494 $dao = new CRM_Core_DAO_PrevNextCache();
495 if ($onlySelected) {
496 $dao->is_selected = 1;
497 }
498 foreach ($prevNextId as $id) {
499 $dao->id = $id;
500 if ($dao->find(TRUE)) {
501 $originalData = unserialize($dao->data);
be2fb01f
CW
502 $srcFields = ['ID', 'Name'];
503 $swapFields = ['srcID', 'srcName', 'dstID', 'dstName'];
808c05a9 504 $data = array_diff_assoc($originalData, array_fill_keys($swapFields, 1));
505 foreach ($srcFields as $key) {
506 $data['src' . $key] = $originalData['dst' . $key];
507 $data['dst' . $key] = $originalData['src' . $key];
508 }
509 $dao->data = serialize($data);
e67dcaf8 510 $dao->entity_id1 = $data['dstID'];
511 $dao->entity_id2 = $data['srcID'];
808c05a9 512 $dao->save();
513 }
514 }
515 }
516
e28bf654
TO
517 /**
518 * Get a list of available backend services.
519 *
520 * @return array
521 * Array(string $id => string $label).
522 */
523 public static function getPrevNextBackends() {
524 return [
525 'default' => ts('Default (Auto-detect)'),
526 'sql' => ts('SQL'),
527 'redis' => ts('Redis'),
528 ];
529 }
530
e13fa54b 531 /**
532 * Generate and assign an arbitrary value to a field of a test object.
533 *
534 * This specifically supports testing the dedupe use case.
535 *
536 * @param string $fieldName
537 * @param array $fieldDef
538 * @param int $counter
539 * The globally-unique ID of the test object.
540 */
541 protected function assignTestValue($fieldName, &$fieldDef, $counter) {
783b4b21
SL
542 if ($fieldName === 'cachekey') {
543 $this->cachekey = 'merge_' . rand();
e13fa54b 544 return;
545 }
546 if ($fieldName === 'data') {
547 $this->data = serialize([]);
548 return;
549 }
550 parent::assignTestValue($fieldName, $fieldDef, $counter);
551 }
552
6a488035 553}