PageRenderTime 251ms CodeModel.GetById 2ms RepoModel.GetById 0ms app.codeStats 1ms

/drupal/sites/all/modules/civicrm/CRM/Logging/Schema.php

https://github.com/michaelmcandrew/lbc
PHP | 370 lines | 228 code | 47 blank | 95 comment | 15 complexity | f41b16a2b2d18aba70b390898bd90dbd MD5 | raw file
  1. <?php
  2. /*
  3. +--------------------------------------------------------------------+
  4. | CiviCRM version 4.1 |
  5. +--------------------------------------------------------------------+
  6. | Copyright CiviCRM LLC (c) 2004-2011 |
  7. +--------------------------------------------------------------------+
  8. | This file is a part of CiviCRM. |
  9. | |
  10. | CiviCRM is free software; you can copy, modify, and distribute it |
  11. | under the terms of the GNU Affero General Public License |
  12. | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
  13. | |
  14. | CiviCRM is distributed in the hope that it will be useful, but |
  15. | WITHOUT ANY WARRANTY; without even the implied warranty of |
  16. | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
  17. | See the GNU Affero General Public License for more details. |
  18. | |
  19. | You should have received a copy of the GNU Affero General Public |
  20. | License and the CiviCRM Licensing Exception along |
  21. | with this program; if not, contact CiviCRM LLC |
  22. | at info[AT]civicrm[DOT]org. If you have questions about the |
  23. | GNU Affero General Public License or the licensing of CiviCRM, |
  24. | see the CiviCRM license FAQ at http://civicrm.org/licensing |
  25. +--------------------------------------------------------------------+
  26. */
  27. /**
  28. *
  29. * @package CRM
  30. * @copyright CiviCRM LLC (c) 2004-2011
  31. * $Id$
  32. *
  33. */
  34. require_once 'CRM/Core/DAO.php';
  35. class CRM_Logging_Schema
  36. {
  37. private $logs = array();
  38. private $tables = array();
  39. private $db;
  40. private $reports = array(
  41. 'logging/contact/detail',
  42. 'logging/contact/summary',
  43. 'logging/contribute/detail',
  44. 'logging/contribute/summary',
  45. );
  46. /**
  47. * Populate $this->tables and $this->logs with current db state.
  48. */
  49. function __construct()
  50. {
  51. require_once 'CRM/Contact/DAO/Contact.php';
  52. $dao = new CRM_Contact_DAO_Contact( );
  53. $civiDBName = $dao->_database;
  54. $dao = CRM_Core_DAO::executeQuery("
  55. SELECT TABLE_NAME
  56. FROM INFORMATION_SCHEMA.TABLES
  57. WHERE TABLE_SCHEMA = '{$civiDBName}'
  58. AND TABLE_TYPE = 'BASE TABLE'
  59. AND TABLE_NAME LIKE 'civicrm_%'
  60. ");
  61. while ($dao->fetch()) {
  62. $this->tables[] = $dao->TABLE_NAME;
  63. }
  64. // do not log temp import, cache and log tables
  65. $this->tables = preg_grep('/^civicrm_import_job_/', $this->tables, PREG_GREP_INVERT);
  66. $this->tables = preg_grep('/_cache$/', $this->tables, PREG_GREP_INVERT);
  67. $this->tables = preg_grep('/_log/', $this->tables, PREG_GREP_INVERT);
  68. $this->tables = preg_grep('/^civicrm_task_action_temp_/', $this->tables, PREG_GREP_INVERT);
  69. $this->tables = preg_grep('/^civicrm_export_temp_/', $this->tables, PREG_GREP_INVERT);
  70. $dsn = defined('CIVICRM_LOGGING_DSN') ? DB::parseDSN(CIVICRM_LOGGING_DSN) : DB::parseDSN(CIVICRM_DSN);
  71. $this->db = $dsn['database'];
  72. $dao = CRM_Core_DAO::executeQuery("
  73. SELECT TABLE_NAME
  74. FROM INFORMATION_SCHEMA.TABLES
  75. WHERE TABLE_SCHEMA = '{$this->db}'
  76. AND TABLE_TYPE = 'BASE TABLE'
  77. AND TABLE_NAME LIKE 'log_civicrm_%'
  78. ");
  79. while ($dao->fetch()) {
  80. $log = $dao->TABLE_NAME;
  81. $this->logs[substr($log, 4)] = $log;
  82. }
  83. }
  84. /**
  85. * Return logging custom data tables.
  86. */
  87. function customDataLogTables()
  88. {
  89. return preg_grep('/^log_civicrm_value_/', $this->logs);
  90. }
  91. /**
  92. * Disable logging by dropping the triggers (but keep the log tables intact).
  93. */
  94. function disableLogging()
  95. {
  96. if (!$this->isEnabled()) return;
  97. $this->dropTriggers();
  98. $this->deleteReports();
  99. }
  100. /**
  101. * Enable logging by creating the log tables (where needed) and creating the triggers.
  102. */
  103. function enableLogging()
  104. {
  105. if ($this->isEnabled()) return;
  106. foreach ($this->tables as $table) {
  107. $this->createLogTableFor($table);
  108. }
  109. $this->createTriggers();
  110. $this->addReports();
  111. }
  112. /**
  113. * Add missing log table columns.
  114. */
  115. function fixSchemaDifferences()
  116. {
  117. if (!$this->isEnabled()) return;
  118. foreach ($this->schemaDifferences() as $table => $cols) {
  119. $this->fixSchemaDifferencesFor($table, $cols);
  120. }
  121. }
  122. /**
  123. * Add missing (potentially specified) log table columns for the given table.
  124. *
  125. * param $table string name of the relevant table
  126. * param $cols mixed array of columns to add or null (to check for the missing columns)
  127. */
  128. function fixSchemaDifferencesFor($table, $cols = null)
  129. {
  130. if (!$this->isEnabled()) return;
  131. if (empty($this->logs[$table])) {
  132. $this->createLogTableFor($table);
  133. }
  134. if (is_null($cols)) {
  135. $cols = array_diff($this->columnsOf($table), $this->columnsOf("log_$table"));
  136. }
  137. if (empty($cols)) return;
  138. // use the relevant lines from CREATE TABLE to add colums to the log table
  139. $dao = CRM_Core_DAO::executeQuery("SHOW CREATE TABLE $table");
  140. $dao->fetch();
  141. $create = explode("\n", $dao->Create_Table);
  142. foreach ($cols as $col) {
  143. $line = substr(array_pop(preg_grep("/^ `$col` /", $create)), 0, -1);
  144. CRM_Core_DAO::executeQuery("ALTER TABLE `{$this->db}`.log_$table ADD $line");
  145. }
  146. // recreate triggers to cater for the new columns
  147. $this->createTriggersFor($table);
  148. }
  149. /**
  150. * Find missing log table columns by comparing columns of the relevant tables.
  151. * Returns table-name-keyed array of arrays of missing columns, e.g. array('civicrm_value_foo_1' => array('bar_1', 'baz_2'))
  152. */
  153. function schemaDifferences()
  154. {
  155. $diffs = array();
  156. foreach ($this->tables as $table) {
  157. $diffs[$table] = array_diff($this->columnsOf($table), $this->columnsOf("log_$table"));
  158. }
  159. return array_filter($diffs);
  160. }
  161. private function addReports()
  162. {
  163. $titles = array(
  164. 'logging/contact/detail' => ts('Contact Logging Report (Detail)'),
  165. 'logging/contact/summary' => ts('Contact Logging Report (Summary)'),
  166. 'logging/contribute/detail' => ts('Contribution Logging Report (Detail)'),
  167. 'logging/contribute/summary' => ts('Contribution Logging Report (Summary)'),
  168. );
  169. // enable logging templates
  170. CRM_Core_DAO::executeQuery("
  171. UPDATE civicrm_option_value
  172. SET is_active = 1
  173. WHERE value IN ('" . implode("', '", $this->reports) . "')
  174. ");
  175. // add report instances
  176. require_once 'CRM/Report/DAO/Instance.php';
  177. $domain_id = CRM_Core_Config::domainID();
  178. foreach ($this->reports as $report) {
  179. $dao = new CRM_Report_DAO_Instance;
  180. $dao->domain_id = $domain_id;
  181. $dao->report_id = $report;
  182. $dao->title = $titles[$report];
  183. $dao->permission = 'administer CiviCRM';
  184. $dao->insert();
  185. }
  186. }
  187. /**
  188. * Get an array of column names of the given table.
  189. */
  190. private function columnsOf($table)
  191. {
  192. static $columnsOf = array();
  193. $from = (substr($table, 0, 4) == 'log_') ? "`{$this->db}`.$table" : $table;
  194. if (!isset($columnsOf[$table])) {
  195. $dao = CRM_Core_DAO::executeQuery("SHOW COLUMNS FROM $from");
  196. $columnsOf[$table] = array();
  197. while ($dao->fetch()) {
  198. $columnsOf[$table][] = $dao->Field;
  199. }
  200. }
  201. return $columnsOf[$table];
  202. }
  203. /**
  204. * Create a log table with schema mirroring the given table’s structure and seeding it with the given table’s contents.
  205. */
  206. private function createLogTableFor($table)
  207. {
  208. CRM_Core_DAO::executeQuery("DROP TABLE IF EXISTS `{$this->db}`.log_$table");
  209. $dao = CRM_Core_DAO::executeQuery("SHOW CREATE TABLE $table");
  210. $dao->fetch();
  211. $query = $dao->Create_Table;
  212. // rewrite the queries into CREATE TABLE queries for log tables:
  213. // - prepend the name with log_
  214. // - drop AUTO_INCREMENT columns
  215. // - drop non-column rows of the query (keys, constraints, etc.)
  216. // - set the ENGINE to ARCHIVE
  217. // - add log-specific columns (at the end of the table)
  218. $cols = <<<COLS
  219. log_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  220. log_conn_id INTEGER,
  221. log_user_id INTEGER,
  222. log_action ENUM('Initialization', 'Insert', 'Update', 'Delete')
  223. COLS;
  224. $query = preg_replace("/^CREATE TABLE `$table`/i", "CREATE TABLE `{$this->db}`.log_$table", $query);
  225. $query = preg_replace("/ AUTO_INCREMENT/i", '', $query);
  226. $query = preg_replace("/^ [^`].*$/m", '', $query);
  227. $query = preg_replace("/^\) ENGINE=[^ ]+ /im", ') ENGINE=ARCHIVE ', $query);
  228. $query = preg_replace("/^\) /m", "$cols\n) ", $query);
  229. CRM_Core_DAO::executeQuery($query);
  230. $columns = implode(', ', $this->columnsOf($table));
  231. CRM_Core_DAO::executeQuery("INSERT INTO `{$this->db}`.log_$table ($columns, log_conn_id, log_user_id, log_action) SELECT $columns, CONNECTION_ID(), @civicrm_user_id, 'Initialization' FROM {$table}");
  232. $this->tables[] = $table;
  233. $this->logs[$table] = "log_$table";
  234. }
  235. /**
  236. * Create triggers populating the relevant log table every time the given table changes.
  237. */
  238. function createTriggersFor($table)
  239. {
  240. if ($table != 'civicrm_contact' and !$this->isEnabled()) return;
  241. $columns = $this->columnsOf($table);
  242. $queries = array();
  243. foreach (array('Insert', 'Update', 'Delete') as $action) {
  244. $trigger = "{$table}_after_" . strtolower($action);
  245. $queries[] = "DROP TRIGGER IF EXISTS $trigger";
  246. $query = "CREATE TRIGGER $trigger AFTER $action ON $table FOR EACH ROW INSERT INTO `{$this->db}`.log_$table (";
  247. foreach ($columns as $column) {
  248. $query .= "$column, ";
  249. }
  250. $query .= "log_conn_id, log_user_id, log_action) VALUES (";
  251. foreach ($columns as $column) {
  252. $query .= $action == 'Delete' ? "OLD.$column, " : "NEW.$column, ";
  253. }
  254. $query .= "CONNECTION_ID(), @civicrm_user_id, '$action')";
  255. $queries[] = $query;
  256. }
  257. $dao = new CRM_Core_DAO;
  258. foreach ($queries as $query) {
  259. $dao->executeQuery($query);
  260. }
  261. }
  262. /**
  263. * Create triggers for all logged tables.
  264. */
  265. private function createTriggers()
  266. {
  267. foreach ($this->tables as $table) {
  268. $this->createTriggersFor($table);
  269. }
  270. }
  271. private function deleteReports()
  272. {
  273. // disable logging templates
  274. CRM_Core_DAO::executeQuery("
  275. UPDATE civicrm_option_value
  276. SET is_active = 0
  277. WHERE value IN ('" . implode("', '", $this->reports) . "')
  278. ");
  279. // delete report instances
  280. require_once 'CRM/Report/DAO/Instance.php';
  281. $domain_id = CRM_Core_Config::domainID();
  282. foreach($this->reports as $report) {
  283. $dao = new CRM_Report_DAO_Instance;
  284. $dao->domain_id = $domain_id;
  285. $dao->report_id = $report;
  286. $dao->delete();
  287. }
  288. }
  289. /**
  290. * Drop triggers for all logged tables.
  291. */
  292. private function dropTriggers()
  293. {
  294. $dao = new CRM_Core_DAO;
  295. foreach ($this->tables as $table) {
  296. $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_insert");
  297. $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_update");
  298. $dao->executeQuery("DROP TRIGGER IF EXISTS {$table}_after_delete");
  299. }
  300. }
  301. /**
  302. * Predicate whether logging is enabled.
  303. */
  304. public function isEnabled()
  305. {
  306. return $this->tablesExist() and $this->triggersExist();
  307. }
  308. /**
  309. * Predicate whether any log tables exist.
  310. */
  311. private function tablesExist()
  312. {
  313. return !empty($this->logs);
  314. }
  315. /**
  316. * Predicate whether the logging triggers are in place.
  317. */
  318. private function triggersExist()
  319. {
  320. // FIXME: probably should be a bit more thorough…
  321. return (bool) CRM_Core_DAO::singleValueQuery("SHOW TRIGGERS LIKE 'civicrm_contact'");
  322. }
  323. }