3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Logging_ReportDetail
extends CRM_Report_Form
{
19 const ROW_COUNT_LIMIT
= 50;
25 * This would be set if we are viewing a merge of 2 contacts.
31 protected $log_conn_id;
34 protected $tables = [];
35 protected $interval = '10 SECOND';
37 protected $altered_name;
38 protected $altered_by;
39 protected $altered_by_id;
42 * detail/summary report ids
51 * @var CRM_Logging_Differ
56 * Array of changes made.
60 protected $diffs = [];
63 * Don't display the Add these contacts to Group button.
67 protected $_add2groupSupported = FALSE;
72 public function __construct() {
76 $this->parsePropertiesFromUrl();
78 parent
::__construct();
80 CRM_Utils_System
::resetBreadCrumb();
83 'title' => ts('Home'),
84 'url' => CRM_Utils_System
::url(),
87 'title' => ts('CiviCRM'),
88 'url' => CRM_Utils_System
::url('civicrm', 'reset=1'),
91 'title' => ts('View Contact'),
92 'url' => CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid={$this->cid}"),
95 'title' => ts('Search Results'),
96 'url' => CRM_Utils_System
::url('civicrm/contact/search', "force=1"),
99 CRM_Utils_System
::appendBreadCrumb($breadcrumb);
101 if (CRM_Utils_Request
::retrieve('revert', 'Boolean')) {
105 $this->_columnHeaders
= [
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
],
113 * Build query for report.
115 * We override this to be empty & calculate the rows in the buildRows function.
117 * @param bool $applyLimit
119 public function buildQuery($applyLimit = TRUE) {
123 * Build rows from query.
128 public function buildRows($sql, &$rows) {
129 // safeguard for when there aren’t any log entries yet
130 if (!$this->log_conn_id
&& !$this->log_date
) {
134 $rows = $this->convertDiffsToRows();
138 * Get the diffs for the report, calculating them if not already done.
140 * Note that contact details report now uses a more comprehensive method but
141 * the contribution logging details report still uses this.
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));
159 protected function diffsInTable($table) {
161 return $this->differ
->diffsInTable($table, $this->cid
);
165 * Convert the diffs to row format.
169 protected function convertDiffsToRows() {
170 // return early if nothing found
171 if (empty($this->diffs
)) {
175 // populate $rows with only the differences between $changed and $original (skipping certain columns and NULL ↔ empty changes unless raw requested)
178 foreach ($this->diffs
as $diff) {
179 $table = $diff['table'];
180 if (empty($metadata[$table])) {
181 list($metadata[$table]['titles'], $metadata[$table]['values']) = $this->differ
->titlesAndValuesForTable($table, $diff['log_date']);
183 $values = CRM_Utils_Array
::value('values', $metadata[$diff['table']], []);
184 $titles = $metadata[$diff['table']]['titles'];
185 $field = $diff['field'];
186 $from = $diff['from'];
190 $field = "$table.$field";
193 if (in_array($field, $skipped)) {
196 // $differ filters out === values; for presentation hide changes like 42 → '42'
201 // special-case for multiple values. Also works for CRM-7251: preferred_communication_method
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
&&
205 substr($to, -1, 1) == CRM_Core_DAO
::VALUE_SEPARATOR
)
208 foreach (explode(CRM_Core_DAO
::VALUE_SEPARATOR
, trim($from, CRM_Core_DAO
::VALUE_SEPARATOR
)) as $val) {
209 $froms[] = $values[$field][$val] ??
NULL;
211 foreach (explode(CRM_Core_DAO
::VALUE_SEPARATOR
, trim($to, CRM_Core_DAO
::VALUE_SEPARATOR
)) as $val) {
212 $tos[] = $values[$field][$val] ??
NULL;
214 $from = implode(', ', array_filter($froms));
215 $to = implode(', ', array_filter($tos));
218 if (isset($values[$field][$from])) {
219 $from = $values[$field][$from];
221 if (isset($values[$field][$to])) {
222 $to = $values[$field][$to];
224 if (isset($titles[$field])) {
225 $field = $titles[$field];
227 if ($diff['action'] == 'Insert') {
230 if ($diff['action'] == 'Delete') {
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];
240 $nRows[$diff['id']][] = ['field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to];
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);
251 $rows[] = $mergedRes;
254 // We always need the first row of that array
255 $rows[] = $trData[0];
262 public function buildQuickForm() {
263 parent
::buildQuickForm();
265 $this->assign('whom_url', CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid={$this->cid}"));
266 $this->assign('who_url', CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid={$this->altered_by_id}"));
267 $this->assign('whom_name', $this->altered_name
);
268 $this->assign('who_name', $this->altered_by
);
270 $this->assign('log_date', CRM_Utils_Date
::mysqlToIso($this->log_date
));
272 $q = "reset=1&log_conn_id={$this->log_conn_id}&log_date={$this->log_date}";
274 $q .= '&oid=' . $this->oid
;
276 $this->assign('revertURL', CRM_Report_Utils_Report
::getNextUrl($this->detail
, "$q&revert=1", FALSE, TRUE));
277 $this->assign('revertConfirm', ts('Are you sure you want to revert all changes?'));
278 $this->assign('sections', []);
282 * Store the dsn for the logging database in $this->db.
284 protected function storeDB() {
285 $dsn = defined('CIVICRM_LOGGING_DSN') ? CRM_Utils_SQL
::autoSwitchDSN(CIVICRM_LOGGING_DSN
) : CRM_Utils_SQL
::autoSwitchDSN(CIVICRM_DSN
);
286 $dsn = DB
::parseDSN($dsn);
287 $this->db
= $dsn['database'];
291 * Calculate all the contact related diffs for the change.
293 protected function calculateContactDiffs() {
294 $this->_rowsFound
= $this->getCountOfAllContactChangesForConnection();
295 // Apply some limits before asking for all contact changes
297 $this->diffs
= $this->getAllContactChangesForConnection();
301 * Get an array of changes made in the mysql connection.
305 public function getAllContactChangesForConnection() {
306 if (empty($this->log_conn_id
)) {
311 return $this->differ
->getAllChangesForConnection($this->tables
, $this->dblimit
, $this->dboffset
);
313 catch (CRM_Core_Exception
$e) {
314 CRM_Core_Error
::statusBounce($e->getMessage());
319 * Get an count of contacts with changes.
323 public function getCountOfAllContactChangesForConnection() {
324 if (empty($this->log_conn_id
)) {
329 return $this->differ
->getCountOfAllContactChangesForConnection($this->tables
);
331 catch (CRM_Core_Exception
$e) {
332 CRM_Core_Error
::statusBounce($e->getMessage());
337 * Make sure the differ is defined.
339 protected function setDiffer() {
340 if (empty($this->differ
)) {
341 $this->differ
= new CRM_Logging_Differ($this->log_conn_id
, $this->log_date
, $this->interval
);
346 * Set this tables to reflect tables changed in a merge.
348 protected function setTablesToContactRelatedTables() {
349 $schema = new CRM_Logging_Schema();
350 $this->tables
= $schema->getLogTablesForContact();
351 // allow tables to be extended by report hook query objects.
352 // This is a report specific hook. It's unclear how it interacts to / overlaps the main one.
353 // It probably precedes the main one and was never reconciled with it....
354 CRM_Report_BAO_Hook
::singleton()->alterLogTables($this, $this->tables
);
358 * Revert the changes defined by the parameters.
360 protected function revert() {
361 $reverter = new CRM_Logging_Reverter($this->log_conn_id
, $this->log_date
);
362 $reverter->calculateDiffsFromLogConnAndDate($this->tables
);
364 CRM_Core_Session
::setStatus(ts('The changes have been reverted.'), ts('Reverted'), 'success');
367 CRM_Utils_System
::redirect(CRM_Utils_System
::url(
368 'civicrm/contact/merge',
369 "reset=1&cid={$this->cid}&oid={$this->oid}",
376 CRM_Utils_System
::redirect(CRM_Utils_System
::url('civicrm/contact/view', "reset=1&selectedChild=log&cid={$this->cid}", FALSE, NULL, FALSE));
380 CRM_Utils_System
::redirect(CRM_Report_Utils_Report
::getNextUrl($this->summary
, 'reset=1', FALSE, TRUE));
385 * Get the properties that might be in the URL.
387 protected function parsePropertiesFromUrl() {
388 $this->log_conn_id
= CRM_Utils_Request
::retrieve('log_conn_id', 'String');
389 $this->log_date
= CRM_Utils_Request
::retrieve('log_date', 'String');
390 $this->cid
= CRM_Utils_Request
::retrieve('cid', 'Integer');
391 $this->raw
= CRM_Utils_Request
::retrieve('raw', 'Boolean');
393 $this->altered_name
= CRM_Utils_Request
::retrieve('alteredName', 'String');
394 $this->altered_by
= CRM_Utils_Request
::retrieve('alteredBy', 'String');
395 $this->altered_by_id
= CRM_Utils_Request
::retrieve('alteredById', 'Integer');
396 $this->layout
= CRM_Utils_Request
::retrieve('layout', 'String');
400 * Override to set limit
401 * @param int $rowCount
403 public function limit($rowCount = self
::ROW_COUNT_LIMIT
) {
404 parent
::limit($rowCount);
408 * Override to set pager with limit
409 * @param int $rowCount
411 public function setPager($rowCount = self
::ROW_COUNT_LIMIT
) {
412 // We should not be rendering the pager in overlay mode
413 if (!isset($this->layout
)) {
414 $this->_dashBoardRowCount
= $rowCount;
415 $this->_limit
= TRUE;
416 parent
::setPager($rowCount);
421 * This is a function similar to limit, in fact we copied it as-is and removed
422 * some `set` statements
425 public function getLimit($rowCount = self
::ROW_COUNT_LIMIT
) {
426 if ($this->addPaging
) {
428 $pageId = CRM_Utils_Request
::retrieve('crmPID', 'Integer');
430 // @todo all http vars should be extracted in the preProcess
431 // - not randomly in the class
432 if (!$pageId && !empty($_POST)) {
433 if (isset($_POST['PagerBottomButton']) && isset($_POST['crmPID_B'])) {
434 $pageId = max((int) $_POST['crmPID_B'], 1);
436 elseif (isset($_POST['PagerTopButton']) && isset($_POST['crmPID'])) {
437 $pageId = max((int) $_POST['crmPID'], 1);
439 unset($_POST['crmPID_B'], $_POST['crmPID']);
442 $pageId = $pageId ?
$pageId : 1;
443 $offset = ($pageId - 1) * $rowCount;
445 $offset = CRM_Utils_Type
::escape($offset, 'Int');
446 $rowCount = CRM_Utils_Type
::escape($rowCount, 'Int');
447 $this->_limit
= " LIMIT $offset, $rowCount";
448 $this->dblimit
= $rowCount;
449 $this->dboffset
= $offset;