Merge pull request #19057 from ixiam/dev/core#2173
[civicrm-core.git] / CRM / Logging / ReportDetail.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 */
17class 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}