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 CW |
105 | $this->_columnHeaders = [ |
106 | 'field' => ['title' => ts('Field')], | |
107 | 'from' => ['title' => ts('Changed From')], | |
108 | 'to' => ['title' => ts('Changed To')], | |
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 | ||
218 | if (isset($values[$field][$from])) { | |
6a488035 | 219 | $from = $values[$field][$from]; |
6a488035 TO |
220 | } |
221 | if (isset($values[$field][$to])) { | |
222 | $to = $values[$field][$to]; | |
223 | } | |
224 | if (isset($titles[$field])) { | |
225 | $field = $titles[$field]; | |
226 | } | |
227 | if ($diff['action'] == 'Insert') { | |
228 | $from = ''; | |
229 | } | |
230 | if ($diff['action'] == 'Delete') { | |
231 | $to = ''; | |
232 | } | |
233 | } | |
2f41db93 VP |
234 | // Rework the results to provide grouping based on the ID |
235 | // We don't need that field displayed so we will output empty | |
236 | if ($field == 'Modified Date') { | |
237 | $nRows[$diff['id']][] = ['field' => '', 'from' => $from, 'to' => $to]; | |
238 | } | |
239 | else { | |
240 | $nRows[$diff['id']][] = ['field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to]; | |
241 | } | |
6a488035 | 242 | } |
2f41db93 VP |
243 | // Transform the output so that we can compact the changes into the proper amount of rows IF trData is holding more than 1 array |
244 | foreach ($nRows as $trData) { | |
245 | if (count($trData) > 1) { | |
246 | $keys = array_intersect(...array_map('array_keys', $trData)); | |
247 | $mergedRes = array_combine($keys, array_map(function ($key) use ($trData) { | |
248 | // If more than 1 entry is found, we are assigning them as subarrays, then the tpls will be responsible for concatenating the results | |
249 | return array_column($trData, $key); | |
250 | }, $keys)); | |
251 | $rows[] = $mergedRes; | |
252 | } | |
253 | else { | |
254 | // We always need the first row of that array | |
255 | $rows[] = $trData[0]; | |
256 | } | |
6a488035 | 257 | |
2f41db93 | 258 | } |
6a488035 TO |
259 | return $rows; |
260 | } | |
261 | ||
00be9182 | 262 | public function buildQuickForm() { |
6a488035 TO |
263 | parent::buildQuickForm(); |
264 | ||
02e02ac5 | 265 | $this->assign('whom_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->cid}")); |
353ffa53 | 266 | $this->assign('who_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->altered_by_id}")); |
02e02ac5 | 267 | $this->assign('whom_name', $this->altered_name); |
353ffa53 | 268 | $this->assign('who_name', $this->altered_by); |
02e02ac5 | 269 | |
6a488035 TO |
270 | $this->assign('log_date', CRM_Utils_Date::mysqlToIso($this->log_date)); |
271 | ||
272 | $q = "reset=1&log_conn_id={$this->log_conn_id}&log_date={$this->log_date}"; | |
549cd4ca | 273 | if ($this->oid) { |
274 | $q .= '&oid=' . $this->oid; | |
275 | } | |
6a488035 | 276 | $this->assign('revertURL', CRM_Report_Utils_Report::getNextUrl($this->detail, "$q&revert=1", FALSE, TRUE)); |
7266e09b | 277 | $this->assign('revertConfirm', ts('Are you sure you want to revert all changes?')); |
6a488035 | 278 | } |
96025800 | 279 | |
e480ef09 | 280 | /** |
281 | * Store the dsn for the logging database in $this->db. | |
282 | */ | |
283 | protected function storeDB() { | |
58d1e21e SL |
284 | $dsn = defined('CIVICRM_LOGGING_DSN') ? CRM_Utils_SQL::autoSwitchDSN(CIVICRM_LOGGING_DSN) : CRM_Utils_SQL::autoSwitchDSN(CIVICRM_DSN); |
285 | $dsn = DB::parseDSN($dsn); | |
e480ef09 | 286 | $this->db = $dsn['database']; |
287 | } | |
288 | ||
aa00132e | 289 | /** |
290 | * Calculate all the contact related diffs for the change. | |
aa00132e | 291 | */ |
549cd4ca | 292 | protected function calculateContactDiffs() { |
2f41db93 VP |
293 | $this->_rowsFound = $this->getCountOfAllContactChangesForConnection(); |
294 | // Apply some limits before asking for all contact changes | |
295 | $this->getLimit(); | |
aa00132e | 296 | $this->diffs = $this->getAllContactChangesForConnection(); |
297 | } | |
298 | ||
aa00132e | 299 | /** |
300 | * Get an array of changes made in the mysql connection. | |
301 | * | |
302 | * @return mixed | |
303 | */ | |
304 | public function getAllContactChangesForConnection() { | |
305 | if (empty($this->log_conn_id)) { | |
be2fb01f | 306 | return []; |
aa00132e | 307 | } |
308 | $this->setDiffer(); | |
309 | try { | |
2f41db93 VP |
310 | return $this->differ->getAllChangesForConnection($this->tables, $this->dblimit, $this->dboffset); |
311 | } | |
312 | catch (CRM_Core_Exception $e) { | |
313 | CRM_Core_Error::statusBounce($e->getMessage()); | |
314 | } | |
315 | } | |
316 | ||
317 | /** | |
318 | * Get an count of contacts with changes. | |
319 | * | |
320 | * @return mixed | |
321 | */ | |
322 | public function getCountOfAllContactChangesForConnection() { | |
323 | if (empty($this->log_conn_id)) { | |
324 | return []; | |
325 | } | |
326 | $this->setDiffer(); | |
327 | try { | |
328 | return $this->differ->getCountOfAllContactChangesForConnection($this->tables); | |
aa00132e | 329 | } |
330 | catch (CRM_Core_Exception $e) { | |
2f41db93 | 331 | CRM_Core_Error::statusBounce($e->getMessage()); |
aa00132e | 332 | } |
333 | } | |
334 | ||
335 | /** | |
336 | * Make sure the differ is defined. | |
337 | */ | |
338 | protected function setDiffer() { | |
339 | if (empty($this->differ)) { | |
340 | $this->differ = new CRM_Logging_Differ($this->log_conn_id, $this->log_date, $this->interval); | |
341 | } | |
342 | } | |
343 | ||
344 | /** | |
345 | * Set this tables to reflect tables changed in a merge. | |
346 | */ | |
347 | protected function setTablesToContactRelatedTables() { | |
348 | $schema = new CRM_Logging_Schema(); | |
349 | $this->tables = $schema->getLogTablesForContact(); | |
350 | // allow tables to be extended by report hook query objects. | |
351 | // This is a report specific hook. It's unclear how it interacts to / overlaps the main one. | |
352 | // It probably precedes the main one and was never reconciled with it.... | |
353 | CRM_Report_BAO_Hook::singleton()->alterLogTables($this, $this->tables); | |
354 | } | |
355 | ||
e480ef09 | 356 | /** |
357 | * Revert the changes defined by the parameters. | |
358 | */ | |
359 | protected function revert() { | |
360 | $reverter = new CRM_Logging_Reverter($this->log_conn_id, $this->log_date); | |
93afbc3a | 361 | $reverter->calculateDiffsFromLogConnAndDate($this->tables); |
362 | $reverter->revert(); | |
e480ef09 | 363 | CRM_Core_Session::setStatus(ts('The changes have been reverted.'), ts('Reverted'), 'success'); |
364 | if ($this->cid) { | |
549cd4ca | 365 | if ($this->oid) { |
366 | CRM_Utils_System::redirect(CRM_Utils_System::url( | |
367 | 'civicrm/contact/merge', | |
368 | "reset=1&cid={$this->cid}&oid={$this->oid}", | |
369 | FALSE, | |
370 | NULL, | |
371 | FALSE) | |
372 | ); | |
373 | } | |
374 | else { | |
375 | CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/view', "reset=1&selectedChild=log&cid={$this->cid}", FALSE, NULL, FALSE)); | |
376 | } | |
e480ef09 | 377 | } |
378 | else { | |
379 | CRM_Utils_System::redirect(CRM_Report_Utils_Report::getNextUrl($this->summary, 'reset=1', FALSE, TRUE)); | |
380 | } | |
381 | } | |
382 | ||
3b45d110 | 383 | /** |
384 | * Get the properties that might be in the URL. | |
385 | */ | |
386 | protected function parsePropertiesFromUrl() { | |
a3d827a7 CW |
387 | $this->log_conn_id = CRM_Utils_Request::retrieve('log_conn_id', 'String'); |
388 | $this->log_date = CRM_Utils_Request::retrieve('log_date', 'String'); | |
389 | $this->cid = CRM_Utils_Request::retrieve('cid', 'Integer'); | |
390 | $this->raw = CRM_Utils_Request::retrieve('raw', 'Boolean'); | |
391 | ||
392 | $this->altered_name = CRM_Utils_Request::retrieve('alteredName', 'String'); | |
393 | $this->altered_by = CRM_Utils_Request::retrieve('alteredBy', 'String'); | |
394 | $this->altered_by_id = CRM_Utils_Request::retrieve('alteredById', 'Integer'); | |
2f41db93 VP |
395 | $this->layout = CRM_Utils_Request::retrieve('layout', 'String'); |
396 | } | |
397 | ||
398 | /** | |
399 | * Override to set limit | |
400 | * @param int $rowCount | |
401 | */ | |
402 | public function limit($rowCount = self::ROW_COUNT_LIMIT) { | |
403 | parent::limit($rowCount); | |
404 | } | |
405 | ||
406 | /** | |
407 | * Override to set pager with limit | |
408 | * @param int $rowCount | |
409 | */ | |
410 | public function setPager($rowCount = self::ROW_COUNT_LIMIT) { | |
411 | // We should not be rendering the pager in overlay mode | |
412 | if (!isset($this->layout)) { | |
413 | $this->_dashBoardRowCount = $rowCount; | |
414 | $this->_limit = TRUE; | |
415 | parent::setPager($rowCount); | |
416 | } | |
417 | } | |
418 | ||
419 | /** | |
420 | * This is a function similar to limit, in fact we copied it as-is and removed | |
421 | * some `set` statements | |
422 | * | |
423 | */ | |
424 | public function getLimit($rowCount = self::ROW_COUNT_LIMIT) { | |
425 | if ($this->addPaging) { | |
426 | ||
427 | $pageId = CRM_Utils_Request::retrieve('crmPID', 'Integer'); | |
428 | ||
429 | // @todo all http vars should be extracted in the preProcess | |
430 | // - not randomly in the class | |
431 | if (!$pageId && !empty($_POST)) { | |
432 | if (isset($_POST['PagerBottomButton']) && isset($_POST['crmPID_B'])) { | |
433 | $pageId = max((int) $_POST['crmPID_B'], 1); | |
434 | } | |
435 | elseif (isset($_POST['PagerTopButton']) && isset($_POST['crmPID'])) { | |
436 | $pageId = max((int) $_POST['crmPID'], 1); | |
437 | } | |
438 | unset($_POST['crmPID_B'], $_POST['crmPID']); | |
439 | } | |
440 | ||
441 | $pageId = $pageId ? $pageId : 1; | |
442 | $offset = ($pageId - 1) * $rowCount; | |
443 | ||
444 | $offset = CRM_Utils_Type::escape($offset, 'Int'); | |
445 | $rowCount = CRM_Utils_Type::escape($rowCount, 'Int'); | |
446 | $this->_limit = " LIMIT $offset, $rowCount"; | |
447 | $this->dblimit = $rowCount; | |
448 | $this->dboffset = $offset; | |
449 | } | |
3b45d110 | 450 | } |
451 | ||
6a488035 | 452 | } |