First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,510 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
* $Id$
*
*/
class CRM_Logging_Differ {
private $db;
private $log_conn_id;
private $log_date;
private $interval;
/**
* Class constructor.
*
* @param string $log_conn_id
* @param string $log_date
* @param string $interval
*/
public function __construct($log_conn_id, $log_date, $interval = '10 SECOND') {
$dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN);
$this->db = $dsn['database'];
$this->log_conn_id = $log_conn_id;
$this->log_date = $log_date;
$this->interval = self::filterInterval($interval);
}
/**
* @param $tables
*
* @return array
*/
public function diffsInTables($tables) {
$diffs = array();
foreach ($tables as $table) {
$diff = $this->diffsInTable($table);
if (!empty($diff)) {
$diffs[$table] = $diff;
}
}
return $diffs;
}
/**
* @param $table
* @param int $contactID
*
* @return array
*/
public function diffsInTable($table, $contactID = NULL) {
$diffs = array();
$params = array(
1 => array($this->log_conn_id, 'String'),
);
$logging = new CRM_Logging_Schema();
$addressCustomTables = $logging->entityCustomDataLogTables('Address');
$contactIdClause = $join = '';
if ($contactID) {
$params[3] = array($contactID, 'Integer');
switch ($table) {
case 'civicrm_contact':
$contactIdClause = "AND id = %3";
break;
case 'civicrm_note':
$contactIdClause = "AND (( entity_id = %3 AND entity_table = 'civicrm_contact' ) OR (entity_id IN (SELECT note.id FROM `{$this->db}`.log_civicrm_note note WHERE note.entity_id = %3 AND note.entity_table = 'civicrm_contact') AND entity_table = 'civicrm_note'))";
break;
case 'civicrm_entity_tag':
$contactIdClause = "AND entity_id = %3 AND entity_table = 'civicrm_contact'";
break;
case 'civicrm_relationship':
$contactIdClause = "AND (contact_id_a = %3 OR contact_id_b = %3)";
break;
case 'civicrm_activity':
$activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
$sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts);
$assigneeID = CRM_Utils_Array::key('Activity Assignees', $activityContacts);
$targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
$join = "
LEFT JOIN civicrm_activity_contact at ON at.activity_id = lt.id AND at.contact_id = %3 AND at.record_type_id = {$targetID}
LEFT JOIN civicrm_activity_contact aa ON aa.activity_id = lt.id AND aa.contact_id = %3 AND aa.record_type_id = {$assigneeID}
LEFT JOIN civicrm_activity_contact source ON source.activity_id = lt.id AND source.contact_id = %3 AND source.record_type_id = {$sourceID} ";
$contactIdClause = "AND (at.id IS NOT NULL OR aa.id IS NOT NULL OR source.id IS NOT NULL)";
break;
case 'civicrm_case':
$contactIdClause = "AND id = (select case_id FROM civicrm_case_contact WHERE contact_id = %3 LIMIT 1)";
break;
default:
if (array_key_exists($table, $addressCustomTables)) {
$join = "INNER JOIN `{$this->db}`.`log_civicrm_address` et ON et.id = lt.entity_id";
$contactIdClause = "AND contact_id = %3";
break;
}
// allow tables to be extended by report hook query objects
list($contactIdClause, $join) = CRM_Report_BAO_Hook::singleton()->logDiffClause($this, $table);
if (empty($contactIdClause)) {
$contactIdClause = "AND contact_id = %3";
}
if (strpos($table, 'civicrm_value') !== FALSE) {
$contactIdClause = "AND entity_id = %3";
}
}
}
$logDateClause = '';
if ($this->log_date) {
$params[2] = array($this->log_date, 'String');
$logDateClause = "
AND lt.log_date BETWEEN DATE_SUB(%2, INTERVAL {$this->interval}) AND DATE_ADD(%2, INTERVAL {$this->interval})
";
}
// find ids in this table that were affected in the given connection (based on connection id and a ±10 s time period around the date)
$sql = "
SELECT DISTINCT lt.id FROM `{$this->db}`.`log_$table` lt
{$join}
WHERE lt.log_conn_id = %1
$logDateClause
{$contactIdClause}";
$dao = CRM_Core_DAO::executeQuery($sql, $params);
while ($dao->fetch()) {
$diffs = array_merge($diffs, $this->diffsInTableForId($table, $dao->id));
}
return $diffs;
}
/**
* @param $table
* @param int $id
*
* @return array
* @throws \CRM_Core_Exception
*/
private function diffsInTableForId($table, $id) {
$diffs = array();
$params = array(
1 => array($this->log_conn_id, 'String'),
3 => array($id, 'Integer'),
);
// look for all the changes in the given connection that happened less than {$this->interval} s later than log_date to the given id to catch multi-query changes
$logDateClause = "";
if ($this->log_date && $this->interval) {
$logDateClause = " AND log_date >= %2 AND log_date < DATE_ADD(%2, INTERVAL {$this->interval})";
$params[2] = array($this->log_date, 'String');
}
$changedSQL = "SELECT * FROM `{$this->db}`.`log_$table` WHERE log_conn_id = %1 $logDateClause AND id = %3 ORDER BY log_date DESC LIMIT 1";
$changedDAO = CRM_Core_DAO::executeQuery($changedSQL, $params);
while ($changedDAO->fetch()) {
if (empty($this->log_date) && !self::checkLogCanBeUsedWithNoLogDate($changedDAO->log_date)) {
throw new CRM_Core_Exception('The connection date must be passed in to disambiguate this logging entry per CRM-18193');
}
$changed = $changedDAO->toArray();
// return early if nothing found
if (empty($changed)) {
continue;
}
switch ($changed['log_action']) {
case 'Delete':
// the previous state is kept in the current state, current should keep the keys and clear the values
$original = $changed;
foreach ($changed as & $val) {
$val = NULL;
}
$changed['log_action'] = 'Delete';
break;
case 'Insert':
// the previous state does not exist
$original = array();
break;
case 'Update':
$params[2] = array($changedDAO->log_date, 'String');
// look for the previous state (different log_conn_id) of the given id
$originalSQL = "SELECT * FROM `{$this->db}`.`log_$table` WHERE log_conn_id != %1 AND log_date < %2 AND id = %3 ORDER BY log_date DESC LIMIT 1";
$original = $this->sqlToArray($originalSQL, $params);
if (empty($original)) {
// A blank original array is not possible for Update action, otherwise we 'll end up displaying all information
// in $changed variable as updated-info
$original = $changed;
}
break;
}
// populate $diffs with only the differences between $changed and $original
$skipped = array('log_action', 'log_conn_id', 'log_date', 'log_user_id');
foreach (array_keys(array_diff_assoc($changed, $original)) as $diff) {
if (in_array($diff, $skipped)) {
continue;
}
if (CRM_Utils_Array::value($diff, $original) === CRM_Utils_Array::value($diff, $changed)) {
continue;
}
// hack: case_type_id column is a varchar with separator. For proper mapping to type labels,
// we need to make sure separators are trimmed
if ($diff == 'case_type_id') {
foreach (array('original', 'changed') as $var) {
if (!empty(${$var[$diff]})) {
$holder =& $$var;
$val = explode(CRM_Case_BAO_Case::VALUE_SEPARATOR, $holder[$diff]);
$holder[$diff] = CRM_Utils_Array::value(1, $val);
}
}
}
$diffs[] = array(
'action' => $changed['log_action'],
'id' => $id,
'field' => $diff,
'from' => CRM_Utils_Array::value($diff, $original),
'to' => CRM_Utils_Array::value($diff, $changed),
'table' => $table,
'log_date' => $changed['log_date'],
'log_conn_id' => $changed['log_conn_id'],
);
}
}
return $diffs;
}
/**
* Get the titles & metadata option values for the table.
*
* For custom fields the titles may change so we use the ones as at the reference date.
*
* @param string $table
* @param string $referenceDate
*
* @return array
*/
public function titlesAndValuesForTable($table, $referenceDate) {
// static caches for subsequent calls with the same $table
static $titles = array();
static $values = array();
if (!isset($titles[$table]) or !isset($values[$table])) {
if (($tableDAO = CRM_Core_DAO_AllCoreTables::getClassForTable($table)) != FALSE) {
// FIXME: these should be populated with pseudo constants as they
// were at the time of logging rather than their current values
// FIXME: Use *_BAO:buildOptions() method rather than pseudoconstants & fetch programmatically
$values[$table] = array(
'contribution_page_id' => CRM_Contribute_PseudoConstant::contributionPage(),
'contribution_status_id' => CRM_Contribute_PseudoConstant::contributionStatus(),
'financial_type_id' => CRM_Contribute_PseudoConstant::financialType(),
'country_id' => CRM_Core_PseudoConstant::country(),
'gender_id' => CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id'),
'location_type_id' => CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id'),
'payment_instrument_id' => CRM_Contribute_PseudoConstant::paymentInstrument(),
'phone_type_id' => CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id'),
'preferred_communication_method' => CRM_Contact_BAO_Contact::buildOptions('preferred_communication_method'),
'preferred_language' => CRM_Contact_BAO_Contact::buildOptions('preferred_language'),
'prefix_id' => CRM_Contact_BAO_Contact::buildOptions('prefix_id'),
'provider_id' => CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id'),
'state_province_id' => CRM_Core_PseudoConstant::stateProvince(),
'suffix_id' => CRM_Contact_BAO_Contact::buildOptions('suffix_id'),
'website_type_id' => CRM_Core_PseudoConstant::get('CRM_Core_DAO_Website', 'website_type_id'),
'activity_type_id' => CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'label', TRUE),
'case_type_id' => CRM_Case_PseudoConstant::caseType('title', FALSE),
'priority_id' => CRM_Core_PseudoConstant::get('CRM_Activity_DAO_Activity', 'priority_id'),
);
// for columns that appear in more than 1 table
switch ($table) {
case 'civicrm_case':
$values[$table]['status_id'] = CRM_Case_PseudoConstant::caseStatus('label', FALSE);
break;
case 'civicrm_activity':
$values[$table]['status_id'] = CRM_Core_PseudoConstant::activityStatus();
break;
}
$dao = new $tableDAO();
foreach ($dao->fields() as $field) {
$titles[$table][$field['name']] = CRM_Utils_Array::value('title', $field);
if ($field['type'] == CRM_Utils_Type::T_BOOLEAN) {
$values[$table][$field['name']] = array('0' => ts('false'), '1' => ts('true'));
}
}
}
elseif (substr($table, 0, 14) == 'civicrm_value_') {
list($titles[$table], $values[$table]) = $this->titlesAndValuesForCustomDataTable($table, $referenceDate);
}
else {
$titles[$table] = $values[$table] = array();
}
}
return array($titles[$table], $values[$table]);
}
/**
* @param $sql
* @param array $params
*
* @return mixed
*/
private function sqlToArray($sql, $params) {
$dao = CRM_Core_DAO::executeQuery($sql, $params);
$dao->fetch();
return $dao->toArray();
}
/**
* Get the field titles & option group values for the custom table as at the reference date.
*
* @param string $table
* @param string $referenceDate
*
* @return array
*/
private function titlesAndValuesForCustomDataTable($table, $referenceDate) {
$titles = array();
$values = array();
$params = array(
1 => array($this->log_conn_id, 'String'),
2 => array($referenceDate, 'String'),
3 => array($table, 'String'),
);
$sql = "SELECT id, title FROM `{$this->db}`.log_civicrm_custom_group WHERE log_date <= %2 AND table_name = %3 ORDER BY log_date DESC LIMIT 1";
$cgDao = CRM_Core_DAO::executeQuery($sql, $params);
$cgDao->fetch();
$params[3] = array($cgDao->id, 'Integer');
$sql = "
SELECT column_name, data_type, label, name, option_group_id
FROM `{$this->db}`.log_civicrm_custom_field
WHERE log_date <= %2
AND custom_group_id = %3
ORDER BY log_date
";
$cfDao = CRM_Core_DAO::executeQuery($sql, $params);
while ($cfDao->fetch()) {
$titles[$cfDao->column_name] = "{$cgDao->title}: {$cfDao->label}";
switch ($cfDao->data_type) {
case 'Boolean':
$values[$cfDao->column_name] = array('0' => ts('false'), '1' => ts('true'));
break;
case 'String':
$values[$cfDao->column_name] = array();
if (!empty($cfDao->option_group_id)) {
$params[3] = array($cfDao->option_group_id, 'Integer');
$sql = "
SELECT label, value
FROM `{$this->db}`.log_civicrm_option_value
WHERE log_date <= %2
AND option_group_id = %3
ORDER BY log_date
";
$ovDao = CRM_Core_DAO::executeQuery($sql, $params);
while ($ovDao->fetch()) {
$values[$cfDao->column_name][$ovDao->value] = $ovDao->label;
}
}
break;
}
}
return array($titles, $values);
}
/**
* Get all changes made in the connection.
*
* @param array $tables
* Array of tables to inspect.
*
* @return array
*/
public function getAllChangesForConnection($tables) {
$params = array(1 => array($this->log_conn_id, 'String'));
foreach ($tables as $table) {
if (empty($sql)) {
$sql = " SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
}
else {
$sql .= " UNION SELECT '{$table}' as table_name, id FROM {$this->db}.log_{$table} WHERE log_conn_id = %1";
}
}
$diffs = array();
$dao = CRM_Core_DAO::executeQuery($sql, $params);
while ($dao->fetch()) {
if (empty($this->log_date)) {
$this->log_date = CRM_Core_DAO::singleValueQuery("SELECT log_date FROM {$this->db}.log_{$table} WHERE log_conn_id = %1 LIMIT 1", $params);
}
$diffs = array_merge($diffs, $this->diffsInTableForId($dao->table_name, $dao->id));
}
return $diffs;
}
/**
* Check that the log record relates to a unique log id.
*
* If the record was recorded using the old non-unique style then the
* log_date
* MUST be set to get the (fairly accurate) list of changes. In this case the
* nasty 10 second interval rule is applied.
*
* See CRM-18193 for a discussion of unique log id.
*
* @param string $change_date
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
public static function checkLogCanBeUsedWithNoLogDate($change_date) {
if (civicrm_api3('Setting', 'getvalue', array('name' => 'logging_all_tables_uniquid', 'group' => 'CiviCRM Preferences'))) {
return TRUE;
};
$uniqueDate = civicrm_api3('Setting', 'getvalue', array(
'name' => 'logging_uniqueid_date',
'group' => 'CiviCRM Preferences',
));
if (strtotime($uniqueDate) <= strtotime($change_date)) {
return TRUE;
}
else {
return FALSE;
}
}
/**
* Filter a MySQL interval expression.
*
* @param string $interval
* @return string
* Normalized version of $interval
* @throws \CRM_Core_Exception
* If the expression is invalid.
* @see https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_date-add
*/
private static function filterInterval($interval) {
if (empty($interval)) {
return $interval;
}
$units = array('MICROSECOND', 'SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR');
$interval = strtoupper($interval);
if (preg_match('/^([0-9]+) ([A-Z]+)$/', $interval, $matches)) {
if (in_array($matches[2], $units)) {
return $interval;
}
}
if (preg_match('/^\'([0-9: \.\-]+)\' ([A-Z]+)_([A-Z]+)$/', $interval, $matches)) {
if (in_array($matches[2], $units) && in_array($matches[3], $units)) {
return $interval;
}
}
throw new CRM_Core_Exception("Malformed interval");
}
}

View file

@ -0,0 +1,365 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class CRM_Logging_ReportDetail extends CRM_Report_Form {
protected $cid;
/**
* Other contact ID.
*
* This would be set if we are viewing a merge of 2 contacts.
*
* @var int
*/
protected $oid;
protected $db;
protected $log_conn_id;
protected $log_date;
protected $raw;
protected $tables = array();
protected $interval = '10 SECOND';
protected $altered_name;
protected $altered_by;
protected $altered_by_id;
// detail/summary report ids
protected $detail;
protected $summary;
/**
* Instance of Differ.
*
* @var CRM_Logging_Differ
*/
protected $differ;
/**
* Array of changes made.
*
* @var array
*/
protected $diffs = array();
/**
* Don't display the Add these contacts to Group button.
*
* @var bool
*/
protected $_add2groupSupported = FALSE;
/**
* Class constructor.
*/
public function __construct() {
$this->storeDB();
$this->parsePropertiesFromUrl();
parent::__construct();
CRM_Utils_System::resetBreadCrumb();
$breadcrumb = array(
array(
'title' => ts('Home'),
'url' => CRM_Utils_System::url(),
),
array(
'title' => ts('CiviCRM'),
'url' => CRM_Utils_System::url('civicrm', 'reset=1'),
),
array(
'title' => ts('View Contact'),
'url' => CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->cid}"),
),
array(
'title' => ts('Search Results'),
'url' => CRM_Utils_System::url('civicrm/contact/search', "force=1"),
),
);
CRM_Utils_System::appendBreadCrumb($breadcrumb);
if (CRM_Utils_Request::retrieve('revert', 'Boolean')) {
$this->revert();
}
$this->_columnHeaders = array(
'field' => array('title' => ts('Field')),
'from' => array('title' => ts('Changed From')),
'to' => array('title' => ts('Changed To')),
);
}
/**
* Build query for report.
*
* We override this to be empty & calculate the rows in the buildRows function.
*
* @param bool $applyLimit
*/
public function buildQuery($applyLimit = TRUE) {
}
/**
* Build rows from query.
*
* @param string $sql
* @param array $rows
*/
public function buildRows($sql, &$rows) {
// safeguard for when there arent any log entries yet
if (!$this->log_conn_id && !$this->log_date) {
return;
}
$this->getDiffs();
$rows = $this->convertDiffsToRows();
}
/**
* Get the diffs for the report, calculating them if not already done.
*
* Note that contact details report now uses a more comprehensive method but
* the contribution logging details report still uses this.
*
* @return array
*/
protected function getDiffs() {
if (empty($this->diffs)) {
foreach ($this->tables as $table) {
$this->diffs = array_merge($this->diffs, $this->diffsInTable($table));
}
}
return $this->diffs;
}
/**
* @param $table
*
* @return array
*/
protected function diffsInTable($table) {
$this->setDiffer();
return $this->differ->diffsInTable($table, $this->cid);
}
/**
* Convert the diffs to row format.
*
* @return array
*/
protected function convertDiffsToRows() {
// return early if nothing found
if (empty($this->diffs)) {
return array();
}
// populate $rows with only the differences between $changed and $original (skipping certain columns and NULL ↔ empty changes unless raw requested)
$skipped = array('id');
foreach ($this->diffs as $diff) {
$table = $diff['table'];
if (empty($metadata[$table])) {
list($metadata[$table]['titles'], $metadata[$table]['values']) = $this->differ->titlesAndValuesForTable($table, $diff['log_date']);
}
$values = CRM_Utils_Array::value('values', $metadata[$diff['table']], array());
$titles = $metadata[$diff['table']]['titles'];
$field = $diff['field'];
$from = $diff['from'];
$to = $diff['to'];
if ($this->raw) {
$field = "$table.$field";
}
else {
if (in_array($field, $skipped)) {
continue;
}
// $differ filters out === values; for presentation hide changes like 42 → '42'
if ($from == $to) {
continue;
}
// special-case for multiple values. Also works for CRM-7251: preferred_communication_method
if ((substr($from, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR &&
substr($from, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR) ||
(substr($to, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR &&
substr($to, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR)
) {
$froms = $tos = array();
foreach (explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($from, CRM_Core_DAO::VALUE_SEPARATOR)) as $val) {
$froms[] = CRM_Utils_Array::value($val, $values[$field]);
}
foreach (explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($to, CRM_Core_DAO::VALUE_SEPARATOR)) as $val) {
$tos[] = CRM_Utils_Array::value($val, $values[$field]);
}
$from = implode(', ', array_filter($froms));
$to = implode(', ', array_filter($tos));
}
if (isset($values[$field][$from])) {
$from = $values[$field][$from];
}
if (isset($values[$field][$to])) {
$to = $values[$field][$to];
}
if (isset($titles[$field])) {
$field = $titles[$field];
}
if ($diff['action'] == 'Insert') {
$from = '';
}
if ($diff['action'] == 'Delete') {
$to = '';
}
}
$rows[] = array('field' => $field . " (id: {$diff['id']})", 'from' => $from, 'to' => $to);
}
return $rows;
}
public function buildQuickForm() {
parent::buildQuickForm();
$this->assign('whom_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->cid}"));
$this->assign('who_url', CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->altered_by_id}"));
$this->assign('whom_name', $this->altered_name);
$this->assign('who_name', $this->altered_by);
$this->assign('log_date', CRM_Utils_Date::mysqlToIso($this->log_date));
$q = "reset=1&log_conn_id={$this->log_conn_id}&log_date={$this->log_date}";
if ($this->oid) {
$q .= '&oid=' . $this->oid;
}
$this->assign('revertURL', CRM_Report_Utils_Report::getNextUrl($this->detail, "$q&revert=1", FALSE, TRUE));
$this->assign('revertConfirm', ts('Are you sure you want to revert all changes?'));
}
/**
* Store the dsn for the logging database in $this->db.
*/
protected function storeDB() {
$dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN);
$this->db = $dsn['database'];
}
/**
* Calculate all the contact related diffs for the change.
*/
protected function calculateContactDiffs() {
$this->diffs = $this->getAllContactChangesForConnection();
}
/**
* Get an array of changes made in the mysql connection.
*
* @return mixed
*/
public function getAllContactChangesForConnection() {
if (empty($this->log_conn_id)) {
return array();
}
$this->setDiffer();
try {
return $this->differ->getAllChangesForConnection($this->tables);
}
catch (CRM_Core_Exception $e) {
CRM_Core_Error::statusBounce(ts($e->getMessage()));
}
}
/**
* Make sure the differ is defined.
*/
protected function setDiffer() {
if (empty($this->differ)) {
$this->differ = new CRM_Logging_Differ($this->log_conn_id, $this->log_date, $this->interval);
}
}
/**
* Set this tables to reflect tables changed in a merge.
*/
protected function setTablesToContactRelatedTables() {
$schema = new CRM_Logging_Schema();
$this->tables = $schema->getLogTablesForContact();
// allow tables to be extended by report hook query objects.
// This is a report specific hook. It's unclear how it interacts to / overlaps the main one.
// It probably precedes the main one and was never reconciled with it....
CRM_Report_BAO_Hook::singleton()->alterLogTables($this, $this->tables);
}
/**
* Revert the changes defined by the parameters.
*/
protected function revert() {
$reverter = new CRM_Logging_Reverter($this->log_conn_id, $this->log_date);
$reverter->calculateDiffsFromLogConnAndDate($this->tables);
$reverter->revert();
CRM_Core_Session::setStatus(ts('The changes have been reverted.'), ts('Reverted'), 'success');
if ($this->cid) {
if ($this->oid) {
CRM_Utils_System::redirect(CRM_Utils_System::url(
'civicrm/contact/merge',
"reset=1&cid={$this->cid}&oid={$this->oid}",
FALSE,
NULL,
FALSE)
);
}
else {
CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/view', "reset=1&selectedChild=log&cid={$this->cid}", FALSE, NULL, FALSE));
}
}
else {
CRM_Utils_System::redirect(CRM_Report_Utils_Report::getNextUrl($this->summary, 'reset=1', FALSE, TRUE));
}
}
/**
* Get the properties that might be in the URL.
*/
protected function parsePropertiesFromUrl() {
$this->log_conn_id = CRM_Utils_Request::retrieve('log_conn_id', 'String');
$this->log_date = CRM_Utils_Request::retrieve('log_date', 'String');
$this->cid = CRM_Utils_Request::retrieve('cid', 'Integer');
$this->raw = CRM_Utils_Request::retrieve('raw', 'Boolean');
$this->altered_name = CRM_Utils_Request::retrieve('alteredName', 'String');
$this->altered_by = CRM_Utils_Request::retrieve('alteredBy', 'String');
$this->altered_by_id = CRM_Utils_Request::retrieve('alteredById', 'Integer');
}
}

View file

@ -0,0 +1,433 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
* $Id$
*/
class CRM_Logging_ReportSummary extends CRM_Report_Form {
protected $cid;
protected $_logTables = array();
protected $loggingDB;
/**
* The log table currently being processed.
*
* @var string
*/
protected $currentLogTable;
/**
* Class constructor.
*/
public function __construct() {
// dont display the Add these Contacts to Group button
$this->_add2groupSupported = FALSE;
$dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN);
$this->loggingDB = $dsn['database'];
// used for redirect back to contact summary
$this->cid = CRM_Utils_Request::retrieve('cid', 'Integer');
$this->_logTables = array(
'log_civicrm_contact' => array(
'fk' => 'id',
),
'log_civicrm_email' => array(
'fk' => 'contact_id',
'log_type' => 'Contact',
),
'log_civicrm_phone' => array(
'fk' => 'contact_id',
'log_type' => 'Contact',
),
'log_civicrm_address' => array(
'fk' => 'contact_id',
'log_type' => 'Contact',
),
'log_civicrm_note' => array(
'fk' => 'entity_id',
'entity_table' => TRUE,
'bracket_info' => array(
'table' => 'log_civicrm_note',
'column' => 'subject',
),
),
'log_civicrm_note_comment' => array(
'fk' => 'entity_id',
'table_name' => 'log_civicrm_note',
'joins' => array(
'table' => 'log_civicrm_note',
'join' => "entity_log_civireport.entity_id = fk_table.id AND entity_log_civireport.entity_table = 'civicrm_note'",
),
'entity_table' => TRUE,
'bracket_info' => array(
'table' => 'log_civicrm_note',
'column' => 'subject',
),
),
'log_civicrm_group_contact' => array(
'fk' => 'contact_id',
'bracket_info' => array(
'entity_column' => 'group_id',
'table' => 'log_civicrm_group',
'column' => 'title',
),
'action_column' => 'status',
'log_type' => 'Group',
),
'log_civicrm_entity_tag' => array(
'fk' => 'entity_id',
'bracket_info' => array(
'entity_column' => 'tag_id',
'table' => 'log_civicrm_tag',
'column' => 'name',
),
'entity_table' => TRUE,
),
'log_civicrm_relationship' => array(
'fk' => 'contact_id_a',
'bracket_info' => array(
'entity_column' => 'relationship_type_id',
'table' => 'log_civicrm_relationship_type',
'column' => 'label_a_b',
),
),
'log_civicrm_activity_contact' => array(
'fk' => 'contact_id',
'table_name' => 'log_civicrm_activity_contact',
'log_type' => 'Activity',
'field' => 'activity_id',
'extra_joins' => array(
'table' => 'log_civicrm_activity',
'join' => 'extra_table.id = entity_log_civireport.activity_id',
),
'bracket_info' => array(
'entity_column' => 'activity_type_id',
'options' => CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'label', TRUE),
'lookup_table' => 'log_civicrm_activity',
),
),
'log_civicrm_case' => array(
'fk' => 'contact_id',
'joins' => array(
'table' => 'log_civicrm_case_contact',
'join' => 'entity_log_civireport.id = fk_table.case_id',
),
'bracket_info' => array(
'entity_column' => 'case_type_id',
'options' => CRM_Case_BAO_Case::buildOptions('case_type_id', 'search'),
),
),
);
$logging = new CRM_Logging_Schema();
// build _logTables for contact custom tables
$customTables = $logging->entityCustomDataLogTables('Contact');
foreach ($customTables as $table) {
$this->_logTables[$table] = array(
'fk' => 'entity_id',
'log_type' => 'Contact',
);
}
// build _logTables for address custom tables
$customTables = $logging->entityCustomDataLogTables('Address');
foreach ($customTables as $table) {
$this->_logTables[$table] = array(
// For join of fk_table with contact table.
'fk' => 'contact_id',
'joins' => array(
// fk_table
'table' => 'log_civicrm_address',
'join' => 'entity_log_civireport.entity_id = fk_table.id',
),
'log_type' => 'Contact',
);
}
// Allow log tables to be extended via report hooks.
CRM_Report_BAO_Hook::singleton()->alterLogTables($this, $this->_logTables);
parent::__construct();
}
public function groupBy() {
$this->_groupBy = 'GROUP BY entity_log_civireport.log_conn_id, entity_log_civireport.log_user_id, EXTRACT(DAY_MICROSECOND FROM entity_log_civireport.log_date), entity_log_civireport.id';
}
/**
* Adjust query for the activity_contact table.
*
* As this is just a join table the ID we REALLY care about is the activity id.
*
* @param string $tableName
* @param string $tableKey
* @param string $fieldName
* @param string $field
*
* @return string
*/
public function selectClause(&$tableName, $tableKey, &$fieldName, &$field) {
if ($this->currentLogTable == 'log_civicrm_activity_contact' && $fieldName == 'id') {
$alias = "{$tableName}_{$fieldName}";
$select[] = "{$tableName}.activity_id as $alias";
$this->_selectAliases[] = $alias;
return "activity_id";
}
if ($fieldName == 'log_grouping') {
if ($this->currentLogTable != 'log_civicrm_activity_contact') {
return 1;
}
$mergeActivityID = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Contact Merged');
return " IF (entity_log_civireport.log_action = 'Insert' AND extra_table.activity_type_id = $mergeActivityID , GROUP_CONCAT(entity_log_civireport.contact_id), 1) ";
}
}
public function where() {
// reset where clause as its called multiple times, every time insert sql is built.
$this->_whereClauses = array();
parent::where();
$this->_where .= " AND (entity_log_civireport.log_action != 'Initialization')";
}
public function postProcess() {
$this->beginPostProcess();
$rows = array();
$tempColumns = "id int(10), log_civicrm_entity_log_grouping varchar(32)";
if (!empty($this->_params['fields']['log_action'])) {
$tempColumns .= ", log_action varchar(64)";
}
$tempColumns .= ", log_type varchar(64), log_user_id int(10), log_date timestamp";
if (!empty($this->_params['fields']['altered_contact'])) {
$tempColumns .= ", altered_contact varchar(128)";
}
$tempColumns .= ", altered_contact_id int(10), log_conn_id varchar(17), is_deleted tinyint(4)";
if (!empty($this->_params['fields']['display_name'])) {
$tempColumns .= ", display_name varchar(128)";
}
// temp table to hold all altered contact-ids
$sql = "CREATE TEMPORARY TABLE civicrm_temp_civireport_logsummary ( {$tempColumns} ) ENGINE=HEAP";
CRM_Core_DAO::executeQuery($sql);
$logTypes = CRM_Utils_Array::value('log_type_value', $this->_params);
unset($this->_params['log_type_value']);
if (empty($logTypes)) {
foreach (array_keys($this->_logTables) as $table) {
$type = $this->getLogType($table);
$logTypes[$type] = $type;
}
}
$logTypeTableClause = '(1)';
if ($logTypeTableValue = CRM_Utils_Array::value("log_type_table_value", $this->_params)) {
$logTypeTableClause = $this->whereClause($this->_columns['log_civicrm_entity']['filters']['log_type_table'],
$this->_params['log_type_table_op'], $logTypeTableValue, NULL, NULL);
unset($this->_params['log_type_table_value']);
}
foreach ($this->_logTables as $entity => $detail) {
if ((in_array($this->getLogType($entity), $logTypes) &&
CRM_Utils_Array::value('log_type_op', $this->_params) == 'in') ||
(!in_array($this->getLogType($entity), $logTypes) &&
CRM_Utils_Array::value('log_type_op', $this->_params) == 'notin')
) {
$this->currentLogTable = $entity;
$sql = $this->buildQuery(FALSE);
$sql = str_replace("entity_log_civireport.log_type as", "'{$entity}' as", $sql);
$sql = "INSERT IGNORE INTO civicrm_temp_civireport_logsummary {$sql}";
CRM_Core_DAO::executeQuery($sql);
}
}
$this->currentLogTable = '';
// add computed log_type column so that we can do a group by after that, which will help
// alterDisplay() counts sync with pager counts
$sql = "SELECT DISTINCT log_type FROM civicrm_temp_civireport_logsummary";
$dao = CRM_Core_DAO::executeQuery($sql);
$replaceWith = array();
while ($dao->fetch()) {
$type = $this->getLogType($dao->log_type);
if (!array_key_exists($type, $replaceWith)) {
$replaceWith[$type] = array();
}
$replaceWith[$type][] = $dao->log_type;
}
foreach ($replaceWith as $type => $tables) {
if (!empty($tables)) {
$replaceWith[$type] = implode("','", $tables);
}
}
$sql = "ALTER TABLE civicrm_temp_civireport_logsummary ADD COLUMN log_civicrm_entity_log_type_label varchar(64)";
CRM_Core_DAO::executeQuery($sql);
foreach ($replaceWith as $type => $in) {
$sql = "UPDATE civicrm_temp_civireport_logsummary SET log_civicrm_entity_log_type_label='{$type}', log_date=log_date WHERE log_type IN('$in')";
CRM_Core_DAO::executeQuery($sql);
}
// note the group by columns are same as that used in alterDisplay as $newRows - $key
$this->limit();
$sql = "{$this->_select}
FROM civicrm_temp_civireport_logsummary entity_log_civireport
WHERE {$logTypeTableClause}
GROUP BY log_civicrm_entity_log_date, log_civicrm_entity_log_type_label, log_civicrm_entity_log_conn_id, log_civicrm_entity_log_user_id, log_civicrm_entity_altered_contact_id, log_civicrm_entity_log_grouping
ORDER BY log_civicrm_entity_log_date DESC {$this->_limit}";
$sql = str_replace('modified_contact_civireport.display_name', 'entity_log_civireport.altered_contact', $sql);
$sql = str_replace('modified_contact_civireport.id', 'entity_log_civireport.altered_contact_id', $sql);
$sql = str_replace(array(
'modified_contact_civireport.',
'altered_by_contact_civireport.',
), 'entity_log_civireport.', $sql);
$this->buildRows($sql, $rows);
// format result set.
$this->formatDisplay($rows);
// assign variables to templates
$this->doTemplateAssignment($rows);
// do print / pdf / instance stuff if needed
$this->endPostProcess($rows);
}
/**
* Get log type.
*
* @param string $entity
*
* @return string
*/
public function getLogType($entity) {
if (!empty($this->_logTables[$entity]['log_type'])) {
return $this->_logTables[$entity]['log_type'];
}
$logType = ucfirst(substr($entity, strrpos($entity, '_') + 1));
return $logType;
}
/**
* Get entity value.
*
* @param int $id
* @param $entity
* @param $logDate
*
* @return mixed|null|string
*/
public function getEntityValue($id, $entity, $logDate) {
if (!empty($this->_logTables[$entity]['bracket_info'])) {
if (!empty($this->_logTables[$entity]['bracket_info']['entity_column'])) {
$logTable = !empty($this->_logTables[$entity]['table_name']) ? $this->_logTables[$entity]['table_name'] : $entity;
if (!empty($this->_logTables[$entity]['bracket_info']['lookup_table'])) {
$logTable = $this->_logTables[$entity]['bracket_info']['lookup_table'];
}
$sql = "
SELECT {$this->_logTables[$entity]['bracket_info']['entity_column']}
FROM `{$this->loggingDB}`.{$logTable}
WHERE log_date <= %1 AND id = %2 ORDER BY log_date DESC LIMIT 1";
$entityID = CRM_Core_DAO::singleValueQuery($sql, array(
1 => array(
CRM_Utils_Date::isoToMysql($logDate),
'Timestamp',
),
2 => array($id, 'Integer'),
));
}
else {
$entityID = $id;
}
if ($entityID && $logDate &&
array_key_exists('table', $this->_logTables[$entity]['bracket_info'])
) {
$sql = "
SELECT {$this->_logTables[$entity]['bracket_info']['column']}
FROM `{$this->loggingDB}`.{$this->_logTables[$entity]['bracket_info']['table']}
WHERE log_date <= %1 AND id = %2 ORDER BY log_date DESC LIMIT 1";
return CRM_Core_DAO::singleValueQuery($sql, array(
1 => array(CRM_Utils_Date::isoToMysql($logDate), 'Timestamp'),
2 => array($entityID, 'Integer'),
));
}
else {
if (array_key_exists('options', $this->_logTables[$entity]['bracket_info']) &&
$entityID
) {
return CRM_Utils_Array::value($entityID, $this->_logTables[$entity]['bracket_info']['options']);
}
}
}
return NULL;
}
/**
* Get entity action.
*
* @param int $id
* @param int $connId
* @param $entity
* @param $oldAction
*
* @return null|string
*/
public function getEntityAction($id, $connId, $entity, $oldAction) {
if (!empty($this->_logTables[$entity]['action_column'])) {
$sql = "select {$this->_logTables[$entity]['action_column']} from `{$this->loggingDB}`.{$entity} where id = %1 AND log_conn_id = %2";
$newAction = CRM_Core_DAO::singleValueQuery($sql, array(
1 => array($id, 'Integer'),
2 => array($connId, 'String'),
));
switch ($entity) {
case 'log_civicrm_group_contact':
if ($oldAction !== 'Update') {
$newAction = $oldAction;
}
if ($oldAction == 'Insert') {
$newAction = 'Added';
}
break;
}
return $newAction;
}
return NULL;
}
}

View file

@ -0,0 +1,205 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class CRM_Logging_Reverter {
private $db;
private $log_conn_id;
private $log_date;
/**
* The diffs to be reverted.
*
* @var array
*/
private $diffs = array();
/**
* Class constructor.
*
* @param string $log_conn_id
* @param $log_date
*/
public function __construct($log_conn_id, $log_date) {
$dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN);
$this->db = $dsn['database'];
$this->log_conn_id = $log_conn_id;
$this->log_date = $log_date;
}
/**
*
* Calculate a set of diffs based on the connection_id and changes at a close time.
*
* @param array $tables
*/
public function calculateDiffsFromLogConnAndDate($tables) {
$differ = new CRM_Logging_Differ($this->log_conn_id, $this->log_date);
$this->diffs = $differ->diffsInTables($tables);
}
/**
* Setter for diffs.
*
* @param array $diffs
*/
public function setDiffs($diffs) {
$this->diffs = $diffs;
}
/**
* Revert changes in the array of diffs in $this->diffs.
*/
public function revert() {
// get custom data tables, columns and types
$ctypes = array();
$dao = CRM_Core_DAO::executeQuery('SELECT table_name, column_name, data_type FROM civicrm_custom_group cg JOIN civicrm_custom_field cf ON (cf.custom_group_id = cg.id)');
while ($dao->fetch()) {
if (!isset($ctypes[$dao->table_name])) {
$ctypes[$dao->table_name] = array('entity_id' => 'Integer');
}
$ctypes[$dao->table_name][$dao->column_name] = $dao->data_type;
}
$diffs = $this->diffs;
$deletes = array();
$reverts = array();
foreach ($diffs as $table => $changes) {
foreach ($changes as $change) {
switch ($change['action']) {
case 'Insert':
if (!isset($deletes[$table])) {
$deletes[$table] = array();
}
$deletes[$table][] = $change['id'];
break;
case 'Delete':
case 'Update':
if (!isset($reverts[$table])) {
$reverts[$table] = array();
}
if (!isset($reverts[$table][$change['id']])) {
$reverts[$table][$change['id']] = array('log_action' => $change['action']);
}
$reverts[$table][$change['id']][$change['field']] = $change['from'];
break;
}
}
}
// revert inserts by deleting
foreach ($deletes as $table => $ids) {
CRM_Core_DAO::executeQuery("DELETE FROM `$table` WHERE id IN (" . implode(', ', array_unique($ids)) . ')');
}
// revert updates by updating to previous values
foreach ($reverts as $table => $row) {
switch (TRUE) {
// DAO-based tables
case (($tableDAO = CRM_Core_DAO_AllCoreTables::getClassForTable($table)) != FALSE):
$dao = new $tableDAO ();
foreach ($row as $id => $changes) {
$dao->id = $id;
foreach ($changes as $field => $value) {
if ($field == 'log_action') {
continue;
}
if (empty($value) and $value !== 0 and $value !== '0') {
$value = 'null';
}
// Date reaches this point in ISO format (possibly) so strip out stuff
// if it does have hyphens of colons demarking the date & it regexes as being a date
// or datetime format.
if (preg_match('/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/', $value)) {
$value = str_replace('-', '', $value);
$value = str_replace(':', '', $value);
}
$dao->$field = $value;
}
$changes['log_action'] == 'Delete' ? $dao->insert() : $dao->update();
$dao->reset();
}
break;
// custom data tables
case in_array($table, array_keys($ctypes)):
foreach ($row as $id => $changes) {
$inserts = array('id' => '%1');
$updates = array();
$params = array(1 => array($id, 'Integer'));
$counter = 2;
foreach ($changes as $field => $value) {
// dont try reverting a field thats no longer there
if (!isset($ctypes[$table][$field])) {
continue;
}
$fldVal = "%{$counter}";
switch ($ctypes[$table][$field]) {
case 'Date':
$value = substr(CRM_Utils_Date::isoToMysql($value), 0, 8);
break;
case 'Timestamp':
$value = CRM_Utils_Date::isoToMysql($value);
break;
case 'Boolean':
if ($value === '') {
$fldVal = 'DEFAULT';
}
}
$inserts[$field] = "%$counter";
$updates[] = "{$field} = {$fldVal}";
if ($fldVal != 'DEFAULT') {
$params[$counter] = array($value, $ctypes[$table][$field]);
}
$counter++;
}
if ($changes['log_action'] == 'Delete') {
$sql = "INSERT INTO `$table` (" . implode(', ', array_keys($inserts)) . ') VALUES (' . implode(', ', $inserts) . ')';
}
else {
$sql = "UPDATE `$table` SET " . implode(', ', $updates) . ' WHERE id = %1';
}
CRM_Core_DAO::executeQuery($sql, $params);
}
break;
}
}
}
}

View file

@ -0,0 +1,949 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class CRM_Logging_Schema {
private $logs = array();
private $tables = array();
private $db;
private $useDBPrefix = TRUE;
private $reports = array(
'logging/contact/detail',
'logging/contact/summary',
'logging/contribute/detail',
'logging/contribute/summary',
);
/**
* Columns that should never be subject to logging.
*
* CRM-13028 / NYSS-6933 - table => array (cols) - to be excluded from the update statement
*
* @var array
*/
private $exceptions = array(
'civicrm_job' => array('last_run'),
'civicrm_group' => array('cache_date', 'refresh_date'),
);
/**
* Specifications of all log table including
* - engine (default is archive, if not set.)
* - engine_config, a string appended to the engine type.
* For INNODB space can be saved with 'ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4'
* - indexes (default is none and they cannot be added unless engine is innodb. If they are added and
* engine is not set to innodb an exception will be thrown since quiet acquiescence is easier to miss).
* - exceptions (by default those stored in $this->exceptions are included). These are
* excluded from the triggers.
*
* @var array
*/
private $logTableSpec = array();
/**
* Setting Callback - Validate.
*
* @param mixed $value
* @param array $fieldSpec
*
* @return bool
* @throws API_Exception
*/
public static function checkLoggingSupport(&$value, $fieldSpec) {
$domain = new CRM_Core_DAO_Domain();
$domain->find(TRUE);
if (!(CRM_Core_DAO::checkTriggerViewPermission(FALSE)) && $value) {
throw new API_Exception("In order to use this functionality, the installation's database user must have privileges to create triggers (in MySQL 5.0 and in MySQL 5.1 if binary logging is enabled this means the SUPER privilege). This install either does not seem to have the required privilege enabled.");
}
return TRUE;
}
/**
* Setting Callback - On Change.
*
* Respond to changes in the "logging" setting. Set up or destroy
* triggers, etal.
*
* @param array $oldValue
* List of component names.
* @param array $newValue
* List of component names.
* @param array $metadata
* Specification of the setting (per *.settings.php).
*/
public static function onToggle($oldValue, $newValue, $metadata) {
if ($oldValue == $newValue) {
return;
}
$logging = new CRM_Logging_Schema();
if ($newValue) {
$logging->enableLogging();
}
else {
$logging->disableLogging();
}
}
/**
* Populate $this->tables and $this->logs with current db state.
*/
public function __construct() {
$dao = new CRM_Contact_DAO_Contact();
$civiDBName = $dao->_database;
$dao = CRM_Core_DAO::executeQuery("
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = '{$civiDBName}'
AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_NAME LIKE 'civicrm_%'
");
while ($dao->fetch()) {
$this->tables[] = $dao->TABLE_NAME;
}
// do not log temp import, cache, menu and log tables
$this->tables = preg_grep('/^civicrm_import_job_/', $this->tables, PREG_GREP_INVERT);
$this->tables = preg_grep('/_cache$/', $this->tables, PREG_GREP_INVERT);
$this->tables = preg_grep('/_log/', $this->tables, PREG_GREP_INVERT);
$this->tables = preg_grep('/^civicrm_queue_/', $this->tables, PREG_GREP_INVERT);
//CRM-14672
$this->tables = preg_grep('/^civicrm_menu/', $this->tables, PREG_GREP_INVERT);
$this->tables = preg_grep('/_temp_/', $this->tables, PREG_GREP_INVERT);
// CRM-18178
$this->tables = preg_grep('/_bak$/', $this->tables, PREG_GREP_INVERT);
$this->tables = preg_grep('/_backup$/', $this->tables, PREG_GREP_INVERT);
// do not log civicrm_mailing_event* tables, CRM-12300
$this->tables = preg_grep('/^civicrm_mailing_event_/', $this->tables, PREG_GREP_INVERT);
// do not log civicrm_mailing_recipients table, CRM-16193
$this->tables = array_diff($this->tables, array('civicrm_mailing_recipients'));
$this->logTableSpec = array_fill_keys($this->tables, array());
foreach ($this->exceptions as $tableName => $fields) {
$this->logTableSpec[$tableName]['exceptions'] = $fields;
}
CRM_Utils_Hook::alterLogTables($this->logTableSpec);
$this->tables = array_keys($this->logTableSpec);
$nonStandardTableNameString = $this->getNonStandardTableNameFilterString();
if (defined('CIVICRM_LOGGING_DSN')) {
$dsn = DB::parseDSN(CIVICRM_LOGGING_DSN);
$this->useDBPrefix = (CIVICRM_LOGGING_DSN != CIVICRM_DSN);
}
else {
$dsn = DB::parseDSN(CIVICRM_DSN);
$this->useDBPrefix = FALSE;
}
$this->db = $dsn['database'];
$dao = CRM_Core_DAO::executeQuery("
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = '{$this->db}'
AND TABLE_TYPE = 'BASE TABLE'
AND (TABLE_NAME LIKE 'log_civicrm_%' $nonStandardTableNameString )
");
while ($dao->fetch()) {
$log = $dao->TABLE_NAME;
$this->logs[substr($log, 4)] = $log;
}
}
/**
* Return logging custom data tables.
*/
public function customDataLogTables() {
return preg_grep('/^log_civicrm_value_/', $this->logs);
}
/**
* Return custom data tables for specified entity / extends.
*
* @param string $extends
*
* @return array
*/
public function entityCustomDataLogTables($extends) {
$customGroupTables = array();
$customGroupDAO = CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity($extends);
$customGroupDAO->find();
while ($customGroupDAO->fetch()) {
$customGroupTables[$customGroupDAO->table_name] = $this->logs[$customGroupDAO->table_name];
}
return $customGroupTables;
}
/**
* Disable logging by dropping the triggers (but keep the log tables intact).
*/
public function disableLogging() {
$config = CRM_Core_Config::singleton();
$config->logging = FALSE;
$this->dropTriggers();
// invoke the meta trigger creation call
CRM_Core_DAO::triggerRebuild();
$this->deleteReports();
}
/**
* Drop triggers for all logged tables.
*
* @param string $tableName
*/
public function dropTriggers($tableName = NULL) {
/** @var \Civi\Core\SqlTriggers $sqlTriggers */
$sqlTriggers = Civi::service('sql_triggers');
$dao = new CRM_Core_DAO();
if ($tableName) {
$tableNames = array($tableName);
}
else {
$tableNames = $this->tables;
}
foreach ($tableNames as $table) {
$validName = CRM_Core_DAO::shortenSQLName($table, 48, TRUE);
// before triggers
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_before_insert");
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_before_update");
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_before_delete");
// after triggers
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_after_insert");
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_after_update");
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$validName}_after_delete");
}
// now lets also be safe and drop all triggers that start with
// civicrm_ if we are dropping all triggers
// we need to do this to capture all the leftover triggers since
// we did the shortening trigger name for CRM-11794
if ($tableName === NULL) {
$triggers = $dao->executeQuery("SHOW TRIGGERS LIKE 'civicrm_%'");
while ($triggers->fetch()) {
$sqlTriggers->enqueueQuery("DROP TRIGGER IF EXISTS {$triggers->Trigger}");
}
}
}
/**
* Enable site-wide logging.
*/
public function enableLogging() {
$this->fixSchemaDifferences(TRUE);
$this->addReports();
}
/**
* Sync log tables and rebuild triggers.
*
* @param bool $enableLogging : Ensure logging is enabled
*/
public function fixSchemaDifferences($enableLogging = FALSE) {
$config = CRM_Core_Config::singleton();
if ($enableLogging) {
$config->logging = TRUE;
}
if ($config->logging) {
$this->fixSchemaDifferencesForALL();
}
// invoke the meta trigger creation call
CRM_Core_DAO::triggerRebuild(NULL, TRUE);
}
/**
* Update log tables structure.
*
* This function updates log tables to have the log_conn_id type of varchar
* and also implements any engine change to INNODB defined by the hooks.
*
* Note changing engine & adding hook-defined indexes, but not changing back
* to ARCHIVE if engine has not been deliberately set (by hook) and not dropping
* indexes. Sysadmin will need to manually intervene to revert to defaults.
*/
public function updateLogTableSchema() {
$updateLogConn = FALSE;
foreach ($this->logs as $mainTable => $logTable) {
$alterSql = array();
$tableSpec = $this->logTableSpec[$mainTable];
if (isset($tableSpec['engine']) && strtoupper($tableSpec['engine']) != $this->getEngineForLogTable($logTable)) {
$alterSql[] = "ENGINE=" . $tableSpec['engine'] . " " . CRM_Utils_Array::value('engine_config', $tableSpec);
if (!empty($tableSpec['indexes'])) {
$indexes = $this->getIndexesForTable($logTable);
foreach ($tableSpec['indexes'] as $indexName => $indexSpec) {
if (!in_array($indexName, $indexes)) {
if (is_array($indexSpec)) {
$indexSpec = implode(" , ", $indexSpec);
}
$alterSql[] = "ADD INDEX {$indexName}($indexSpec)";
}
}
}
}
$columns = $this->columnSpecsOf($logTable);
if (empty($columns['log_conn_id'])) {
throw new Exception($logTable . print_r($columns, TRUE));
}
if ($columns['log_conn_id']['DATA_TYPE'] != 'varchar' || $columns['log_conn_id']['LENGTH'] != 17) {
$alterSql[] = "MODIFY log_conn_id VARCHAR(17)";
$updateLogConn = TRUE;
}
if (!empty($alterSql)) {
CRM_Core_DAO::executeQuery("ALTER TABLE {$this->db}.{$logTable} " . implode(', ', $alterSql));
}
}
if ($updateLogConn) {
civicrm_api3('Setting', 'create', array('logging_uniqueid_date' => date('Y-m-d H:i:s')));
}
}
/**
* Get the engine for the given table.
*
* @param string $table
*
* @return string
*/
public function getEngineForLogTable($table) {
return strtoupper(CRM_Core_DAO::singleValueQuery("
SELECT ENGINE FROM information_schema.tables WHERE TABLE_NAME = %1
AND table_schema = %2
", array(1 => array($table, 'String'), 2 => array($this->db, 'String'))));
}
/**
* Get all the indexes in the table.
*
* @param string $table
*
* @return array
*/
public function getIndexesForTable($table) {
return CRM_Core_DAO::executeQuery("
SELECT constraint_name
FROM information_schema.key_column_usage
WHERE table_schema = %2 AND table_name = %1",
array(1 => array($table, 'String'), 2 => array($this->db, 'String'))
)->fetchAll();
}
/**
* Add missing (potentially specified) log table columns for the given table.
*
* @param string $table
* name of the relevant table.
* @param array $cols
* Mixed array of columns to add or null (to check for the missing columns).
* @param bool $rebuildTrigger
* should we rebuild the triggers.
*
* @return bool
*/
public function fixSchemaDifferencesFor($table, $cols = array(), $rebuildTrigger = FALSE) {
if (empty($table)) {
return FALSE;
}
if (empty($this->logs[$table])) {
$this->createLogTableFor($table);
return TRUE;
}
if (empty($cols)) {
$cols = $this->columnsWithDiffSpecs($table, "log_$table");
}
// use the relevant lines from CREATE TABLE to add colums to the log table
$create = $this->_getCreateQuery($table);
foreach ((array('ADD', 'MODIFY')) as $alterType) {
if (!empty($cols[$alterType])) {
foreach ($cols[$alterType] as $col) {
$line = $this->_getColumnQuery($col, $create);
CRM_Core_DAO::executeQuery("ALTER TABLE `{$this->db}`.log_$table {$alterType} {$line}", array(), TRUE, NULL, FALSE, FALSE);
}
}
}
// for any obsolete columns (not null) we just make the column nullable.
if (!empty($cols['OBSOLETE'])) {
$create = $this->_getCreateQuery("`{$this->db}`.log_{$table}");
foreach ($cols['OBSOLETE'] as $col) {
$line = $this->_getColumnQuery($col, $create);
// This is just going to make a not null column to nullable
CRM_Core_DAO::executeQuery("ALTER TABLE `{$this->db}`.log_$table MODIFY {$line}", array(), TRUE, NULL, FALSE, FALSE);
}
}
if ($rebuildTrigger) {
// invoke the meta trigger creation call
CRM_Core_DAO::triggerRebuild($table);
}
return TRUE;
}
/**
* Get query table.
*
* @param string $table
*
* @return array
*/
private function _getCreateQuery($table) {
$dao = CRM_Core_DAO::executeQuery("SHOW CREATE TABLE {$table}", array(), TRUE, NULL, FALSE, FALSE);
$dao->fetch();
$create = explode("\n", $dao->Create_Table);
return $create;
}
/**
* Get column query.
*
* @param string $col
* @param bool $createQuery
*
* @return array|mixed|string
*/
private function _getColumnQuery($col, $createQuery) {
$line = preg_grep("/^ `$col` /", $createQuery);
$line = rtrim(array_pop($line), ',');
// CRM-11179
$line = $this->fixTimeStampAndNotNullSQL($line);
return $line;
}
/**
* Fix schema differences.
*
* @param bool $rebuildTrigger
*/
public function fixSchemaDifferencesForAll($rebuildTrigger = FALSE) {
$diffs = array();
foreach ($this->tables as $table) {
if (empty($this->logs[$table])) {
$this->createLogTableFor($table);
}
else {
$diffs[$table] = $this->columnsWithDiffSpecs($table, "log_$table");
}
}
foreach ($diffs as $table => $cols) {
$this->fixSchemaDifferencesFor($table, $cols, FALSE);
}
if ($rebuildTrigger) {
// invoke the meta trigger creation call
CRM_Core_DAO::triggerRebuild(NULL, TRUE);
}
}
/**
* Fix timestamp.
*
* Log_civicrm_contact.modified_date for example would always be copied from civicrm_contact.modified_date,
* so there's no need for a default timestamp and therefore we remove such default timestamps
* also eliminate the NOT NULL constraint, since we always copy and schema can change down the road)
*
* @param string $query
*
* @return mixed
*/
public function fixTimeStampAndNotNullSQL($query) {
$query = str_ireplace("TIMESTAMP NOT NULL", "TIMESTAMP NULL", $query);
$query = str_ireplace("DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", '', $query);
$query = str_ireplace("DEFAULT CURRENT_TIMESTAMP", '', $query);
$query = str_ireplace("NOT NULL", '', $query);
return $query;
}
/**
* Add reports.
*/
private function addReports() {
$titles = array(
'logging/contact/detail' => ts('Logging Details'),
'logging/contact/summary' => ts('Contact Logging Report (Summary)'),
'logging/contribute/detail' => ts('Contribution Logging Report (Detail)'),
'logging/contribute/summary' => ts('Contribution Logging Report (Summary)'),
);
// enable logging templates
CRM_Core_DAO::executeQuery("
UPDATE civicrm_option_value
SET is_active = 1
WHERE value IN ('" . implode("', '", $this->reports) . "')
");
// add report instances
$domain_id = CRM_Core_Config::domainID();
foreach ($this->reports as $report) {
$dao = new CRM_Report_DAO_ReportInstance();
$dao->domain_id = $domain_id;
$dao->report_id = $report;
$dao->title = $titles[$report];
$dao->permission = 'administer CiviCRM';
if ($report == 'logging/contact/summary') {
$dao->is_reserved = 1;
}
$dao->insert();
}
}
/**
* Get an array of column names of the given table.
*
* @param string $table
* @param bool $force
*
* @return array
*/
private function columnsOf($table, $force = FALSE) {
if ($force || !isset(\Civi::$statics[__CLASS__]['columnsOf'][$table])) {
$from = (substr($table, 0, 4) == 'log_') ? "`{$this->db}`.$table" : $table;
CRM_Core_TemporaryErrorScope::ignoreException();
$dao = CRM_Core_DAO::executeQuery("SHOW COLUMNS FROM $from", CRM_Core_DAO::$_nullArray, TRUE, NULL, FALSE, FALSE);
if (is_a($dao, 'DB_Error')) {
return array();
}
\Civi::$statics[__CLASS__]['columnsOf'][$table] = array();
while ($dao->fetch()) {
\Civi::$statics[__CLASS__]['columnsOf'][$table][] = CRM_Utils_type::escape($dao->Field, 'MysqlColumnNameOrAlias');
}
}
return \Civi::$statics[__CLASS__]['columnsOf'][$table];
}
/**
* Get an array of columns and their details like DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT for the given table.
*
* @param string $table
*
* @return array
*/
private function columnSpecsOf($table) {
static $civiDB = NULL;
if (empty(\Civi::$statics[__CLASS__]['columnSpecs'])) {
\Civi::$statics[__CLASS__]['columnSpecs'] = array();
}
if (empty(\Civi::$statics[__CLASS__]['columnSpecs']) || !isset(\Civi::$statics[__CLASS__]['columnSpecs'][$table])) {
if (!$civiDB) {
$dao = new CRM_Contact_DAO_Contact();
$civiDB = $dao->_database;
}
CRM_Core_TemporaryErrorScope::ignoreException();
// NOTE: W.r.t Performance using one query to find all details and storing in static array is much faster
// than firing query for every given table.
$query = "
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema IN ('{$this->db}', '{$civiDB}')";
$dao = CRM_Core_DAO::executeQuery($query);
if (is_a($dao, 'DB_Error')) {
return array();
}
while ($dao->fetch()) {
if (!array_key_exists($dao->TABLE_NAME, \Civi::$statics[__CLASS__]['columnSpecs'])) {
\Civi::$statics[__CLASS__]['columnSpecs'][$dao->TABLE_NAME] = array();
}
\Civi::$statics[__CLASS__]['columnSpecs'][$dao->TABLE_NAME][$dao->COLUMN_NAME] = array(
'COLUMN_NAME' => $dao->COLUMN_NAME,
'DATA_TYPE' => $dao->DATA_TYPE,
'IS_NULLABLE' => $dao->IS_NULLABLE,
'COLUMN_DEFAULT' => $dao->COLUMN_DEFAULT,
);
if (($first = strpos($dao->COLUMN_TYPE, '(')) != 0) {
\Civi::$statics[__CLASS__]['columnSpecs'][$dao->TABLE_NAME][$dao->COLUMN_NAME]['LENGTH'] = substr(
$dao->COLUMN_TYPE, $first, strpos($dao->COLUMN_TYPE, ')')
);
}
}
}
return \Civi::$statics[__CLASS__]['columnSpecs'][$table];
}
/**
* Get columns that have changed.
*
* @param string $civiTable
* @param string $logTable
*
* @return array
*/
public function columnsWithDiffSpecs($civiTable, $logTable) {
$civiTableSpecs = $this->columnSpecsOf($civiTable);
$logTableSpecs = $this->columnSpecsOf($logTable);
$diff = array('ADD' => array(), 'MODIFY' => array(), 'OBSOLETE' => array());
// columns to be added
$diff['ADD'] = array_diff(array_keys($civiTableSpecs), array_keys($logTableSpecs));
// columns to be modified
// NOTE: we consider only those columns for modifications where there is a spec change, and that the column definition
// wasn't deliberately modified by fixTimeStampAndNotNullSQL() method.
foreach ($civiTableSpecs as $col => $colSpecs) {
if (!isset($logTableSpecs[$col]) || !is_array($logTableSpecs[$col])) {
$logTableSpecs[$col] = array();
}
$specDiff = array_diff($civiTableSpecs[$col], $logTableSpecs[$col]);
if (!empty($specDiff) && $col != 'id' && !array_key_exists($col, $diff['ADD'])) {
// ignore 'id' column for any spec changes, to avoid any auto-increment mysql errors
if ($civiTableSpecs[$col]['DATA_TYPE'] != CRM_Utils_Array::value('DATA_TYPE', $logTableSpecs[$col])
// We won't alter the log if the length is decreased in case some of the existing data won't fit.
|| CRM_Utils_Array::value('LENGTH', $civiTableSpecs[$col]) > CRM_Utils_Array::value('LENGTH', $logTableSpecs[$col])
) {
// if data-type is different, surely consider the column
$diff['MODIFY'][] = $col;
}
elseif ($civiTableSpecs[$col]['IS_NULLABLE'] != CRM_Utils_Array::value('IS_NULLABLE', $logTableSpecs[$col]) &&
$logTableSpecs[$col]['IS_NULLABLE'] == 'NO'
) {
// if is-null property is different, and log table's column is NOT-NULL, surely consider the column
$diff['MODIFY'][] = $col;
}
elseif ($civiTableSpecs[$col]['COLUMN_DEFAULT'] != CRM_Utils_Array::value('COLUMN_DEFAULT', $logTableSpecs[$col]) &&
!strstr($civiTableSpecs[$col]['COLUMN_DEFAULT'], 'TIMESTAMP')
) {
// if default property is different, and its not about a timestamp column, consider it
$diff['MODIFY'][] = $col;
}
}
}
// columns to made obsolete by turning into not-null
$oldCols = array_diff(array_keys($logTableSpecs), array_keys($civiTableSpecs));
foreach ($oldCols as $col) {
if (!in_array($col, array('log_date', 'log_conn_id', 'log_user_id', 'log_action')) &&
$logTableSpecs[$col]['IS_NULLABLE'] == 'NO'
) {
// if its a column present only in log table, not among those used by log tables for special purpose, and not-null
$diff['OBSOLETE'][] = $col;
}
}
return $diff;
}
/**
* Getter for logTableSpec.
*
* @return array
*/
public function getLogTableSpec() {
return $this->logTableSpec;
}
/**
* Create a log table with schema mirroring the given tables structure and seeding it with the given tables contents.
*
* @param string $table
*/
private function createLogTableFor($table) {
$dao = CRM_Core_DAO::executeQuery("SHOW CREATE TABLE $table", CRM_Core_DAO::$_nullArray, TRUE, NULL, FALSE, FALSE);
$dao->fetch();
$query = $dao->Create_Table;
// rewrite the queries into CREATE TABLE queries for log tables:
$cols = <<<COLS
,
log_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
log_conn_id VARCHAR(17),
log_user_id INTEGER,
log_action ENUM('Initialization', 'Insert', 'Update', 'Delete')
COLS;
if (!empty($this->logTableSpec[$table]['indexes'])) {
foreach ($this->logTableSpec[$table]['indexes'] as $indexName => $indexSpec) {
if (is_array($indexSpec)) {
$indexSpec = implode(" , ", $indexSpec);
}
$cols .= ", INDEX {$indexName}($indexSpec)";
}
}
// - prepend the name with log_
// - drop AUTO_INCREMENT columns
// - drop non-column rows of the query (keys, constraints, etc.)
// - set the ENGINE to the specified engine (default is archive)
// - add log-specific columns (at the end of the table)
$query = preg_replace("/^CREATE TABLE `$table`/i", "CREATE TABLE `{$this->db}`.log_$table", $query);
$query = preg_replace("/ AUTO_INCREMENT/i", '', $query);
$query = preg_replace("/^ [^`].*$/m", '', $query);
$engine = strtoupper(CRM_Utils_Array::value('engine', $this->logTableSpec[$table], 'ARCHIVE'));
$engine .= " " . CRM_Utils_Array::value('engine_config', $this->logTableSpec[$table]);
$query = preg_replace("/^\) ENGINE=[^ ]+ /im", ') ENGINE=' . $engine . ' ', $query);
// log_civicrm_contact.modified_date for example would always be copied from civicrm_contact.modified_date,
// so there's no need for a default timestamp and therefore we remove such default timestamps
// also eliminate the NOT NULL constraint, since we always copy and schema can change down the road)
$query = self::fixTimeStampAndNotNullSQL($query);
$query = preg_replace("/(,*\n*\) )ENGINE/m", "$cols\n) ENGINE", $query);
CRM_Core_DAO::executeQuery($query, CRM_Core_DAO::$_nullArray, TRUE, NULL, FALSE, FALSE);
$columns = implode(', ', $this->columnsOf($table));
CRM_Core_DAO::executeQuery("INSERT INTO `{$this->db}`.log_$table ($columns, log_conn_id, log_user_id, log_action) SELECT $columns, @uniqueID, @civicrm_user_id, 'Initialization' FROM {$table}", CRM_Core_DAO::$_nullArray, TRUE, NULL, FALSE, FALSE);
$this->tables[] = $table;
if (empty($this->logs)) {
civicrm_api3('Setting', 'create', array('logging_uniqueid_date' => date('Y-m-d H:i:s')));
civicrm_api3('Setting', 'create', array('logging_all_tables_uniquid' => 1));
}
$this->logs[$table] = "log_$table";
}
/**
* Delete reports.
*/
private function deleteReports() {
// disable logging templates
CRM_Core_DAO::executeQuery("
UPDATE civicrm_option_value
SET is_active = 0
WHERE value IN ('" . implode("', '", $this->reports) . "')
");
// delete report instances
$domain_id = CRM_Core_Config::domainID();
foreach ($this->reports as $report) {
$dao = new CRM_Report_DAO_ReportInstance();
$dao->domain_id = $domain_id;
$dao->report_id = $report;
$dao->delete();
}
}
/**
* Predicate whether logging is enabled.
*/
public function isEnabled() {
if (\Civi::settings()->get('logging')) {
return ($this->tablesExist() && (\Civi::settings()->get('logging_no_trigger_permission') || $this->triggersExist()));
}
return FALSE;
}
/**
* Predicate whether any log tables exist.
*/
private function tablesExist() {
return !empty($this->logs);
}
/**
* Drop all log tables.
*
* This does not currently have a usage outside the tests.
*/
public function dropAllLogTables() {
if ($this->tablesExist()) {
foreach ($this->logs as $log_table) {
CRM_Core_DAO::executeQuery("DROP TABLE $log_table");
}
}
}
/**
* Get an sql clause to find the names of any log tables that do not match the normal pattern.
*
* Most tables are civicrm_xxx with the log table being log_civicrm_xxx
* However, they don't have to match this pattern (e.g when defined by hook) so find the
* anomalies and return a filter string to include them.
*
* @return string
*/
public function getNonStandardTableNameFilterString() {
$nonStandardTableNames = preg_grep('/^civicrm_/', $this->tables, PREG_GREP_INVERT);
if (empty($nonStandardTableNames)) {
return '';
}
$nonStandardTableLogs = array();
foreach ($nonStandardTableNames as $nonStandardTableName) {
$nonStandardTableLogs[] = "'log_{$nonStandardTableName}'";
}
return " OR TABLE_NAME IN (" . implode(',', $nonStandardTableLogs) . ")";
}
/**
* Predicate whether the logging triggers are in place.
*/
private function triggersExist() {
// FIXME: probably should be a bit more thorough…
// note that the LIKE parameter is TABLE NAME
return (bool) CRM_Core_DAO::singleValueQuery("SHOW TRIGGERS LIKE 'civicrm_contact'");
}
/**
* Get trigger info.
*
* @param array $info
* @param null $tableName
* @param bool $force
*/
public function triggerInfo(&$info, $tableName = NULL, $force = FALSE) {
if (!CRM_Core_Config::singleton()->logging) {
return;
}
$insert = array('INSERT');
$update = array('UPDATE');
$delete = array('DELETE');
if ($tableName) {
$tableNames = array($tableName);
}
else {
$tableNames = $this->tables;
}
// logging is enabled, so now lets create the trigger info tables
foreach ($tableNames as $table) {
$columns = $this->columnsOf($table, $force);
// only do the change if any data has changed
$cond = array();
foreach ($columns as $column) {
$tableExceptions = array_key_exists('exceptions', $this->logTableSpec[$table]) ? $this->logTableSpec[$table]['exceptions'] : array();
// ignore modified_date changes
if ($column != 'modified_date' && !in_array($column, $tableExceptions)) {
$cond[] = "IFNULL(OLD.$column,'') <> IFNULL(NEW.$column,'')";
}
}
$suppressLoggingCond = "@civicrm_disable_logging IS NULL OR @civicrm_disable_logging = 0";
$updateSQL = "IF ( (" . implode(' OR ', $cond) . ") AND ( $suppressLoggingCond ) ) THEN ";
if ($this->useDBPrefix) {
$sqlStmt = "INSERT INTO `{$this->db}`.log_{tableName} (";
}
else {
$sqlStmt = "INSERT INTO log_{tableName} (";
}
foreach ($columns as $column) {
$sqlStmt .= "$column, ";
}
$sqlStmt .= "log_conn_id, log_user_id, log_action) VALUES (";
$insertSQL = $deleteSQL = "IF ( $suppressLoggingCond ) THEN $sqlStmt ";
$updateSQL .= $sqlStmt;
$sqlStmt = '';
foreach ($columns as $column) {
$sqlStmt .= "NEW.$column, ";
$deleteSQL .= "OLD.$column, ";
}
if (civicrm_api3('Setting', 'getvalue', array('name' => 'logging_uniqueid_date'))) {
// Note that when connecting directly via mysql @uniqueID may not be set so a fallback is
// 'c_' to identify a non-CRM connection + timestamp to the hour + connection_id
// If the connection_id is longer than 6 chars it will be truncated.
// We tried setting the @uniqueID in the trigger but it was unreliable.
// An external interaction could split over 2 connections & it seems worth blocking the revert on
// these reports & adding extra permissioning to the api for this.
$connectionSQLString = "COALESCE(@uniqueID, LEFT(CONCAT('c_', unix_timestamp()/3600, CONNECTION_ID()), 17))";
}
else {
// The log tables have not yet been converted to have varchar(17) fields for log_conn_id.
// Continue to use the less reliable connection_id for al tables for now.
$connectionSQLString = "CONNECTION_ID()";
}
$sqlStmt .= $connectionSQLString . ", @civicrm_user_id, '{eventName}'); END IF;";
$deleteSQL .= $connectionSQLString . ", @civicrm_user_id, '{eventName}'); END IF;";
$insertSQL .= $sqlStmt;
$updateSQL .= $sqlStmt;
$info[] = array(
'table' => array($table),
'when' => 'AFTER',
'event' => $insert,
'sql' => $insertSQL,
);
$info[] = array(
'table' => array($table),
'when' => 'AFTER',
'event' => $update,
'sql' => $updateSQL,
);
$info[] = array(
'table' => array($table),
'when' => 'AFTER',
'event' => $delete,
'sql' => $deleteSQL,
);
}
}
/**
* Disable logging temporarily.
*
* This allow logging to be temporarily disabled for certain cases
* where we want to do a mass cleanup but do not want to bother with
* an audit trail.
*/
public static function disableLoggingForThisConnection() {
if (CRM_Core_Config::singleton()->logging) {
CRM_Core_DAO::executeQuery('SET @civicrm_disable_logging = 1');
}
}
/**
* Get all the log tables that reference civicrm_contact.
*
* Note that it might make sense to wrap this in a getLogTablesForEntity
* but this is the only entity currently available...
*/
public function getLogTablesForContact() {
$tables = array_keys(CRM_Dedupe_Merger::cidRefs());
return array_intersect($tables, $this->tables);
}
/**
* Retrieve missing log tables.
*
* @return array
*/
public function getMissingLogTables() {
if ($this->tablesExist()) {
return array_diff($this->tables, array_keys($this->logs));
}
return array();
}
}