Commit | Line | Data |
---|---|---|
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 | class CRM_Logging_ReportDetail extends CRM_Report_Form { | |
2f41db93 VP |
18 | |
19 | const ROW_COUNT_LIMIT = 50; | |
6a488035 | 20 | protected $cid; |
549cd4ca | 21 | |
22 | /** | |
23 | * Other contact ID. | |
24 | * | |
25 | * This would be set if we are viewing a merge of 2 contacts. | |
26 | * | |
27 | * @var int | |
28 | */ | |
29 | protected $oid; | |
6a488035 TO |
30 | protected $db; |
31 | protected $log_conn_id; | |
32 | protected $log_date; | |
33 | protected $raw; | |
be2fb01f | 34 | protected $tables = []; |
6a488035 TO |
35 | protected $interval = '10 SECOND'; |
36 | ||
02e02ac5 DS |
37 | protected $altered_name; |
38 | protected $altered_by; | |
39 | protected $altered_by_id; | |
40 | ||
971e129b SL |
41 | /** |
42 | * detail/summary report ids | |
43 | * @var int | |
44 | */ | |
6a488035 TO |
45 | protected $detail; |
46 | protected $summary; | |
47 | ||
aa00132e | 48 | /** |
49 | * Instance of Differ. | |
50 | * | |
51 | * @var CRM_Logging_Differ | |
52 | */ | |
53 | protected $differ; | |
54 | ||
55 | /** | |
56 | * Array of changes made. | |
57 | * | |
58 | * @var array | |
59 | */ | |
be2fb01f | 60 | protected $diffs = []; |
aa00132e | 61 | |
e0ef6999 | 62 | /** |
e480ef09 | 63 | * Don't display the Add these contacts to Group button. |
64 | * | |
65 | * @var bool | |
66 | */ | |
67 | protected $_add2groupSupported = FALSE; | |
68 | ||
69 | /** | |
70 | * Class constructor. | |
e0ef6999 | 71 | */ |
00be9182 | 72 | public function __construct() { |
6a488035 | 73 | |
e480ef09 | 74 | $this->storeDB(); |
6a488035 | 75 | |
3b45d110 | 76 | $this->parsePropertiesFromUrl(); |
e0ef6999 | 77 | |
6a488035 TO |
78 | parent::__construct(); |
79 | ||
80 | CRM_Utils_System::resetBreadCrumb(); | |
be2fb01f CW |
81 | $breadcrumb = [ |
82 | [ | |
bed98343 | 83 | 'title' => ts('Home'), |
84 | 'url' => CRM_Utils_System::url(), | |
be2fb01f CW |
85 | ], |
86 | [ | |
bed98343 | 87 | 'title' => ts('CiviCRM'), |
88 | 'url' => CRM_Utils_System::url('civicrm', 'reset=1'), | |
be2fb01f CW |
89 | ], |
90 | [ | |
bed98343 | 91 | 'title' => ts('View Contact'), |
92 | 'url' => CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->cid}"), | |
be2fb01f CW |
93 | ], |
94 | [ | |
bed98343 | 95 | 'title' => ts('Search Results'), |
96 | 'url' => CRM_Utils_System::url('civicrm/contact/search', "force=1"), | |
be2fb01f CW |
97 | ], |
98 | ]; | |
6a488035 TO |
99 | CRM_Utils_System::appendBreadCrumb($breadcrumb); |
100 | ||
e480ef09 | 101 | if (CRM_Utils_Request::retrieve('revert', 'Boolean')) { |
102 | $this->revert(); | |
6a488035 TO |
103 | } |
104 | ||
be2fb01f | 105 | $this->_columnHeaders = [ |
3f9312f2 | 106 | 'field' => ['title' => ts('Field'), 'type' => CRM_Utils_Type::T_STRING], |
107 | 'from' => ['title' => ts('Changed From'), 'type' => CRM_Utils_Type::T_STRING], | |
108 | 'to' => ['title' => ts('Changed To'), 'type' => CRM_Utils_Type::T_STRING], | |
be2fb01f | 109 | ]; |
6a488035 TO |
110 | } |
111 | ||
e0ef6999 | 112 | /** |
aa00132e | 113 | * Build query for report. |
114 | * | |
115 | * We override this to be empty & calculate the rows in the buildRows function. | |
116 | * | |
e0ef6999 EM |
117 | * @param bool $applyLimit |
118 | */ | |
6ea503d4 TO |
119 | public function buildQuery($applyLimit = TRUE) { |
120 | } | |
6a488035 | 121 | |
e0ef6999 | 122 | /** |
549cd4ca | 123 | * Build rows from query. |
124 | * | |
125 | * @param string $sql | |
126 | * @param array $rows | |
e0ef6999 | 127 | */ |
00be9182 | 128 | public function buildRows($sql, &$rows) { |
6a488035 | 129 | // safeguard for when there aren’t any log entries yet |
10b32ed4 | 130 | if (!$this->log_conn_id && !$this->log_date) { |
6a488035 TO |
131 | return; |
132 | } | |
aa00132e | 133 | $this->getDiffs(); |
134 | $rows = $this->convertDiffsToRows(); | |
135 | } | |
6a488035 | 136 | |
aa00132e | 137 | /** |
138 | * Get the diffs for the report, calculating them if not already done. | |
139 | * | |
140 | * Note that contact details report now uses a more comprehensive method but | |
141 | * the contribution logging details report still uses this. | |
142 | * | |
143 | * @return array | |
144 | */ | |
145 | protected function getDiffs() { | |
146 | if (empty($this->diffs)) { | |
147 | foreach ($this->tables as $table) { | |
148 | $this->diffs = array_merge($this->diffs, $this->diffsInTable($table)); | |
149 | } | |
6a488035 | 150 | } |
aa00132e | 151 | return $this->diffs; |
6a488035 TO |
152 | } |
153 | ||
e0ef6999 EM |
154 | /** |
155 | * @param $table | |
156 | * | |
157 | * @return array | |
158 | */ | |
6a488035 | 159 | protected function diffsInTable($table) { |
aa00132e | 160 | $this->setDiffer(); |
161 | return $this->differ->diffsInTable($table, $this->cid); | |
162 | } | |
6a488035 | 163 | |
aa00132e | 164 | /** |
165 | * Convert the diffs to row format. | |
166 | * | |
167 | * @return array | |
168 | */ | |
169 | protected function convertDiffsToRows() { | |
6a488035 | 170 | // return early if nothing found |
aa00132e | 171 | if (empty($this->diffs)) { |
be2fb01f | 172 | return []; |
6a488035 TO |
173 | } |
174 | ||
6a488035 | 175 | // populate $rows with only the differences between $changed and $original (skipping certain columns and NULL ↔ empty changes unless raw requested) |
be2fb01f | 176 | $skipped = ['id']; |
2f41db93 | 177 | $nRows = $rows = []; |
aa00132e | 178 | foreach ($this->diffs as $diff) { |
179 | $table = $diff['table']; | |
180 | if (empty($metadata[$table])) { | |
10b32ed4 | 181 | list($metadata[$table]['titles'], $metadata[$table]['values']) = $this->differ->titlesAndValuesForTable($table, $diff['log_date']); |
aa00132e | 182 | } |
be2fb01f | 183 | $values = CRM_Utils_Array::value('values', $metadata[$diff['table']], []); |
aa00132e | 184 | $titles = $metadata[$diff['table']]['titles']; |
6a488035 | 185 | $field = $diff['field']; |
353ffa53 TO |
186 | $from = $diff['from']; |
187 | $to = $diff['to']; | |
6a488035 TO |
188 | |
189 | if ($this->raw) { | |
190 | $field = "$table.$field"; | |
191 | } | |
192 | else { | |
193 | if (in_array($field, $skipped)) { | |
194 | continue; | |
195 | } | |
196 | // $differ filters out === values; for presentation hide changes like 42 → '42' | |
197 | if ($from == $to) { | |
198 | continue; | |
199 | } | |
200 | ||
40554aa3 | 201 | // special-case for multiple values. Also works for CRM-7251: preferred_communication_method |
e0ef6999 EM |
202 | if ((substr($from, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR && |
203 | substr($from, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR) || | |
204 | (substr($to, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR && | |
353ffa53 TO |
205 | substr($to, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR) |
206 | ) { | |
be2fb01f | 207 | $froms = $tos = []; |
40554aa3 | 208 | foreach (explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($from, CRM_Core_DAO::VALUE_SEPARATOR)) as $val) { |
9c1bc317 | 209 | $froms[] = $values[$field][$val] ?? NULL; |
40554aa3 DS |
210 | } |
211 | foreach (explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($to, CRM_Core_DAO::VALUE_SEPARATOR)) as $val) { | |
9c1bc317 | 212 | $tos[] = $values[$field][$val] ?? NULL; |
40554aa3 | 213 | } |
6a488035 | 214 | $from = implode(', ', array_filter($froms)); |
353ffa53 | 215 | $to = implode(', ', array_filter($tos)); |
6a488035 TO |
216 | } |
217 | ||
a864eb38 | 218 | $tableDAOClass = CRM_Core_DAO_AllCoreTables::getClassForTable($table); |
219 | if (!empty($tableDAOClass)) { | |
220 | $tableDAOFields = (new $tableDAOClass())->fields(); | |
221 | // If this field is a foreign key, then we can later use the foreign | |
222 | // class to translate the id into something more useful for display. | |
223 | $fkClassName = $tableDAOFields[$field]['FKClassName'] ?? NULL; | |
224 | } | |
6a488035 | 225 | if (isset($values[$field][$from])) { |
6a488035 | 226 | $from = $values[$field][$from]; |
6a488035 | 227 | } |
a864eb38 | 228 | elseif (!empty($from) && !empty($fkClassName)) { |
229 | $from = $this->convertForeignKeyValuesToLabels($fkClassName, $field, $from); | |
230 | } | |
6a488035 TO |
231 | if (isset($values[$field][$to])) { |
232 | $to = $values[$field][$to]; | |
233 | } | |
a864eb38 | 234 | elseif (!empty($to) && !empty($fkClassName)) { |
235 | $to = $this->convertForeignKeyValuesToLabels($fkClassName, $field, $to); | |
236 | } | |
6a488035 TO |
237 | if (isset($titles[$field])) { |
238 | $field = $titles[$field]; | |
239 | } | |
240 | if ($diff['action'] == 'Insert') { | |
241 | $from = ''; | |
242 | } | |
243 | if ($diff['action'] == 'Delete') { | |
244 | $to = ''; | |
245 | } | |
246 | } | |
2f41db93 VP |
247 | // Rework the results to provide grouping based on the ID |
248 | // We don't need that field displayed so we will output empty | |
249 | if ($field == 'Modified Date') { | |
250 | $nRows[$diff['id']][] = ['field' => '', 'from' => $from, 'to' => $to]; | |
251 | } | |
252 | else { | |
253 | $nRows[$diff['id']][] = ['field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to]; | |
254 | } | |
6a488035 | 255 | } |
2f41db93 VP |
256 | // Transform the output so that we can compact the changes into the proper amount of rows IF trData is holding more than 1 array |
257 | foreach ($nRows as $trData) { | |
258 | if (count($trData) > 1) { | |
259 | $keys = array_intersect(...array_map('array_keys', $trData)); | |
260 | $mergedRes = array_combine($keys, array_map(function ($key) use ($trData) { | |
261 | // If more than 1 entry is found, we are assigning them as subarrays, then the tpls will be responsible for concatenating the results | |
262 | return array_column($trData, $key); | |
263 | }, $keys)); | |
264 | $rows[] = $mergedRes; | |
265 | } | |
266 | else { | |
267 | // We always need the first row of that array | |
268 | $rows[] = $trData[0]; | |
269 | } | |
6a488035 | 270 | |
2f41db93 | 271 | } |
6a488035 TO |
272 | return $rows; |
273 | } | |
274 | ||
00be9182 | 275 | public function buildQuickForm() { |
6a488035 TO |
276 | parent::buildQuickForm(); |
277 | ||
02e02ac5 | 278 | $this->assign('whom_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->cid}")); |
353ffa53 | 279 | $this->assign('who_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->altered_by_id}")); |
02e02ac5 | 280 | $this->assign('whom_name', $this->altered_name); |
353ffa53 | 281 | $this->assign('who_name', $this->altered_by); |
02e02ac5 | 282 | |
6a488035 TO |
283 | $this->assign('log_date', CRM_Utils_Date::mysqlToIso($this->log_date)); |
284 | ||
285 | $q = "reset=1&log_conn_id={$this->log_conn_id}&log_date={$this->log_date}"; | |
549cd4ca | 286 | if ($this->oid) { |
287 | $q .= '&oid=' . $this->oid; | |
288 | } | |
6a488035 | 289 | $this->assign('revertURL', CRM_Report_Utils_Report::getNextUrl($this->detail, "$q&revert=1", FALSE, TRUE)); |
7266e09b | 290 | $this->assign('revertConfirm', ts('Are you sure you want to revert all changes?')); |
fd68e05b | 291 | $this->assign('sections', []); |
6a488035 | 292 | } |
96025800 | 293 | |
e480ef09 | 294 | /** |
295 | * Store the dsn for the logging database in $this->db. | |
296 | */ | |
297 | protected function storeDB() { | |
58d1e21e SL |
298 | $dsn = defined('CIVICRM_LOGGING_DSN') ? CRM_Utils_SQL::autoSwitchDSN(CIVICRM_LOGGING_DSN) : CRM_Utils_SQL::autoSwitchDSN(CIVICRM_DSN); |
299 | $dsn = DB::parseDSN($dsn); | |
e480ef09 | 300 | $this->db = $dsn['database']; |
301 | } | |
302 | ||
aa00132e | 303 | /** |
304 | * Calculate all the contact related diffs for the change. | |
aa00132e | 305 | */ |
549cd4ca | 306 | protected function calculateContactDiffs() { |
2f41db93 VP |
307 | $this->_rowsFound = $this->getCountOfAllContactChangesForConnection(); |
308 | // Apply some limits before asking for all contact changes | |
309 | $this->getLimit(); | |
aa00132e | 310 | $this->diffs = $this->getAllContactChangesForConnection(); |
311 | } | |
312 | ||
aa00132e | 313 | /** |
314 | * Get an array of changes made in the mysql connection. | |
315 | * | |
316 | * @return mixed | |
317 | */ | |
318 | public function getAllContactChangesForConnection() { | |
319 | if (empty($this->log_conn_id)) { | |
be2fb01f | 320 | return []; |
aa00132e | 321 | } |
322 | $this->setDiffer(); | |
323 | try { | |
2f41db93 VP |
324 | return $this->differ->getAllChangesForConnection($this->tables, $this->dblimit, $this->dboffset); |
325 | } | |
326 | catch (CRM_Core_Exception $e) { | |
327 | CRM_Core_Error::statusBounce($e->getMessage()); | |
328 | } | |
329 | } | |
330 | ||
331 | /** | |
332 | * Get an count of contacts with changes. | |
333 | * | |
334 | * @return mixed | |
335 | */ | |
336 | public function getCountOfAllContactChangesForConnection() { | |
337 | if (empty($this->log_conn_id)) { | |
338 | return []; | |
339 | } | |
340 | $this->setDiffer(); | |
341 | try { | |
342 | return $this->differ->getCountOfAllContactChangesForConnection($this->tables); | |
aa00132e | 343 | } |
344 | catch (CRM_Core_Exception $e) { | |
2f41db93 | 345 | CRM_Core_Error::statusBounce($e->getMessage()); |
aa00132e | 346 | } |
347 | } | |
348 | ||
349 | /** | |
350 | * Make sure the differ is defined. | |
351 | */ | |
352 | protected function setDiffer() { | |
353 | if (empty($this->differ)) { | |
354 | $this->differ = new CRM_Logging_Differ($this->log_conn_id, $this->log_date, $this->interval); | |
355 | } | |
356 | } | |
357 | ||
358 | /** | |
359 | * Set this tables to reflect tables changed in a merge. | |
360 | */ | |
361 | protected function setTablesToContactRelatedTables() { | |
362 | $schema = new CRM_Logging_Schema(); | |
363 | $this->tables = $schema->getLogTablesForContact(); | |
364 | // allow tables to be extended by report hook query objects. | |
365 | // This is a report specific hook. It's unclear how it interacts to / overlaps the main one. | |
366 | // It probably precedes the main one and was never reconciled with it.... | |
367 | CRM_Report_BAO_Hook::singleton()->alterLogTables($this, $this->tables); | |
368 | } | |
369 | ||
e480ef09 | 370 | /** |
371 | * Revert the changes defined by the parameters. | |
372 | */ | |
373 | protected function revert() { | |
374 | $reverter = new CRM_Logging_Reverter($this->log_conn_id, $this->log_date); | |
93afbc3a | 375 | $reverter->calculateDiffsFromLogConnAndDate($this->tables); |
376 | $reverter->revert(); | |
e480ef09 | 377 | CRM_Core_Session::setStatus(ts('The changes have been reverted.'), ts('Reverted'), 'success'); |
378 | if ($this->cid) { | |
549cd4ca | 379 | if ($this->oid) { |
380 | CRM_Utils_System::redirect(CRM_Utils_System::url( | |
381 | 'civicrm/contact/merge', | |
382 | "reset=1&cid={$this->cid}&oid={$this->oid}", | |
383 | FALSE, | |
384 | NULL, | |
385 | FALSE) | |
386 | ); | |
387 | } | |
388 | else { | |
389 | CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/view', "reset=1&selectedChild=log&cid={$this->cid}", FALSE, NULL, FALSE)); | |
390 | } | |
e480ef09 | 391 | } |
392 | else { | |
393 | CRM_Utils_System::redirect(CRM_Report_Utils_Report::getNextUrl($this->summary, 'reset=1', FALSE, TRUE)); | |
394 | } | |
395 | } | |
396 | ||
3b45d110 | 397 | /** |
398 | * Get the properties that might be in the URL. | |
399 | */ | |
400 | protected function parsePropertiesFromUrl() { | |
a3d827a7 CW |
401 | $this->log_conn_id = CRM_Utils_Request::retrieve('log_conn_id', 'String'); |
402 | $this->log_date = CRM_Utils_Request::retrieve('log_date', 'String'); | |
403 | $this->cid = CRM_Utils_Request::retrieve('cid', 'Integer'); | |
404 | $this->raw = CRM_Utils_Request::retrieve('raw', 'Boolean'); | |
405 | ||
406 | $this->altered_name = CRM_Utils_Request::retrieve('alteredName', 'String'); | |
407 | $this->altered_by = CRM_Utils_Request::retrieve('alteredBy', 'String'); | |
408 | $this->altered_by_id = CRM_Utils_Request::retrieve('alteredById', 'Integer'); | |
2f41db93 VP |
409 | $this->layout = CRM_Utils_Request::retrieve('layout', 'String'); |
410 | } | |
411 | ||
412 | /** | |
413 | * Override to set limit | |
414 | * @param int $rowCount | |
415 | */ | |
416 | public function limit($rowCount = self::ROW_COUNT_LIMIT) { | |
417 | parent::limit($rowCount); | |
418 | } | |
419 | ||
420 | /** | |
421 | * Override to set pager with limit | |
422 | * @param int $rowCount | |
423 | */ | |
424 | public function setPager($rowCount = self::ROW_COUNT_LIMIT) { | |
425 | // We should not be rendering the pager in overlay mode | |
426 | if (!isset($this->layout)) { | |
427 | $this->_dashBoardRowCount = $rowCount; | |
428 | $this->_limit = TRUE; | |
429 | parent::setPager($rowCount); | |
430 | } | |
431 | } | |
432 | ||
433 | /** | |
434 | * This is a function similar to limit, in fact we copied it as-is and removed | |
435 | * some `set` statements | |
436 | * | |
437 | */ | |
438 | public function getLimit($rowCount = self::ROW_COUNT_LIMIT) { | |
439 | if ($this->addPaging) { | |
440 | ||
441 | $pageId = CRM_Utils_Request::retrieve('crmPID', 'Integer'); | |
442 | ||
443 | // @todo all http vars should be extracted in the preProcess | |
444 | // - not randomly in the class | |
445 | if (!$pageId && !empty($_POST)) { | |
446 | if (isset($_POST['PagerBottomButton']) && isset($_POST['crmPID_B'])) { | |
447 | $pageId = max((int) $_POST['crmPID_B'], 1); | |
448 | } | |
449 | elseif (isset($_POST['PagerTopButton']) && isset($_POST['crmPID'])) { | |
450 | $pageId = max((int) $_POST['crmPID'], 1); | |
451 | } | |
452 | unset($_POST['crmPID_B'], $_POST['crmPID']); | |
453 | } | |
454 | ||
455 | $pageId = $pageId ? $pageId : 1; | |
456 | $offset = ($pageId - 1) * $rowCount; | |
457 | ||
458 | $offset = CRM_Utils_Type::escape($offset, 'Int'); | |
459 | $rowCount = CRM_Utils_Type::escape($rowCount, 'Int'); | |
460 | $this->_limit = " LIMIT $offset, $rowCount"; | |
461 | $this->dblimit = $rowCount; | |
462 | $this->dboffset = $offset; | |
463 | } | |
3b45d110 | 464 | } |
465 | ||
a864eb38 | 466 | /** |
467 | * Given a key value that we know is a foreign key to another table, return | |
468 | * what the DAO thinks is the "label" for the foreign entity. For example | |
469 | * if it's referencing a contact then return the contact name, or if it's an | |
470 | * activity then return the activity subject. | |
471 | * If it's the type of DAO that doesn't have such a thing, just echo back | |
472 | * what we were given. | |
473 | * | |
474 | * @param string $fkClassName | |
475 | * @param string $field | |
476 | * @param int $keyval | |
477 | * @return string | |
478 | */ | |
479 | private function convertForeignKeyValuesToLabels(string $fkClassName, string $field, int $keyval): string { | |
480 | if (property_exists($fkClassName, '_labelField')) { | |
481 | $labelValue = CRM_Core_DAO::getFieldValue($fkClassName, $keyval, $fkClassName::$_labelField); | |
482 | // Not sure if this should use ts - there's not a lot of context (`%1 (id: %2)`) - and also the similar field labels above don't use ts. | |
483 | return "{$labelValue} (id: {$keyval})"; | |
484 | } | |
485 | return (string) $keyval; | |
486 | } | |
487 | ||
6a488035 | 488 | } |