First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
510
sites/all/modules/civicrm/CRM/Logging/Differ.php
Normal file
510
sites/all/modules/civicrm/CRM/Logging/Differ.php
Normal 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");
|
||||
}
|
||||
|
||||
}
|
365
sites/all/modules/civicrm/CRM/Logging/ReportDetail.php
Normal file
365
sites/all/modules/civicrm/CRM/Logging/ReportDetail.php
Normal 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 aren’t 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');
|
||||
}
|
||||
|
||||
}
|
433
sites/all/modules/civicrm/CRM/Logging/ReportSummary.php
Normal file
433
sites/all/modules/civicrm/CRM/Logging/ReportSummary.php
Normal 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() {
|
||||
// don’t 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;
|
||||
}
|
||||
|
||||
}
|
205
sites/all/modules/civicrm/CRM/Logging/Reverter.php
Normal file
205
sites/all/modules/civicrm/CRM/Logging/Reverter.php
Normal 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) {
|
||||
// don’t try reverting a field that’s 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
949
sites/all/modules/civicrm/CRM/Logging/Schema.php
Normal file
949
sites/all/modules/civicrm/CRM/Logging/Schema.php
Normal 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 table’s structure and seeding it with the given table’s 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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue