2 /* vim: set expandtab sw=4 ts=4 sts=4: */
5 * @author Alexander Rutkowski
11 * Gets relation settings
13 require_once './libraries/relation.lib.php';
16 * This class tracks changes on databases, tables and views.
17 * For more information please see phpMyAdmin/Documentation.html
19 * @author Alexander Rutkowski <alexander.rutkowski@googlemail.com>
22 * @todo use stristr instead of strstr
27 * Whether tracking is ready.
29 static protected $enabled = false;
32 * Defines the internal PMA table which contains tracking data.
37 static protected $pma_table;
40 * Defines the usage of DROP TABLE statment in SQL dumps.
45 static protected $add_drop_table;
48 * Defines the usage of DROP VIEW statment in SQL dumps.
53 static protected $add_drop_view;
56 * Defines the usage of DROP DATABASE statment in SQL dumps.
61 static protected $add_drop_database;
64 * Defines auto-creation of tracking versions.
68 static protected $version_auto_create;
71 * Defines the default set of tracked statements.
75 static protected $default_tracking_set;
78 * Initializes settings. See phpMyAdmin/Documentation.html.
83 static public function init()
85 self
::$pma_table = PMA_backquote($GLOBALS['cfg']['Server']['pmadb']) .".".
86 PMA_backquote($GLOBALS['cfg']['Server']['tracking']);
88 self
::$add_drop_table = $GLOBALS['cfg']['Server']['tracking_add_drop_table'];
90 self
::$add_drop_view = $GLOBALS['cfg']['Server']['tracking_add_drop_view'];
92 self
::$add_drop_database = $GLOBALS['cfg']['Server']['tracking_add_drop_database'];
94 self
::$default_tracking_set = $GLOBALS['cfg']['Server']['tracking_default_statements'];
96 self
::$version_auto_create = $GLOBALS['cfg']['Server']['tracking_version_auto_create'];
101 * Actually enables tracking. This needs to be done after all
102 * underlaying code is initialized.
107 static public function enable()
109 self
::$enabled = true;
113 * Gets the on/off value of the Tracker module, starts initialization.
117 * @return boolean (true=on|false=off)
119 static public function isActive()
121 if (! self
::$enabled) {
124 /* We need to avoid attempt to track any queries from PMA_getRelationsParam */
125 self
::$enabled = false;
126 $cfgRelation = PMA_getRelationsParam();
127 /* Restore original state */
128 self
::$enabled = true;
129 if (! $cfgRelation['trackingwork']) {
134 if (isset(self
::$pma_table)) {
142 * Returns a simple DROP TABLE statement.
144 * @param string $tablename
147 static public function getStatementDropTable($tablename)
149 return 'DROP TABLE IF EXISTS ' . $tablename;
153 * Returns a simple DROP VIEW statement.
155 * @param string $viewname
158 static public function getStatementDropView($viewname)
160 return 'DROP VIEW IF EXISTS ' . $viewname;
164 * Returns a simple DROP DATABASE statement.
166 * @param string $dbname
169 static public function getStatementDropDatabase($dbname)
171 return 'DROP DATABASE IF EXISTS ' . $dbname;
175 * Parses the name of a table from a SQL statement substring.
179 * @param string $string part of SQL statement
181 * @return string the name of table
183 static protected function getTableName($string)
185 if (strstr($string, '.')) {
186 $temp = explode('.', $string);
187 $tablename = $temp[1];
190 $tablename = $string;
193 $str = explode("\n", $tablename);
194 $tablename = $str[0];
196 $tablename = str_replace(';', '', $tablename);
197 $tablename = str_replace('`', '', $tablename);
198 $tablename = trim($tablename);
205 * Gets the tracking status of a table, is it active or deactive ?
209 * @param string $dbname name of database
210 * @param string $tablename name of table
212 * @return boolean true or false
214 static public function isTracked($dbname, $tablename)
216 if (! self
::$enabled) {
219 /* We need to avoid attempt to track any queries from PMA_getRelationsParam */
220 self
::$enabled = false;
221 $cfgRelation = PMA_getRelationsParam();
222 /* Restore original state */
223 self
::$enabled = true;
224 if (! $cfgRelation['trackingwork']) {
229 " SELECT tracking_active FROM " . self
::$pma_table .
230 " WHERE " . PMA_backquote('db_name') . " = '" . PMA_sqlAddslashes($dbname) . "' " .
231 " AND " . PMA_backquote('table_name') . " = '" . PMA_sqlAddslashes($tablename) . "' " .
232 " ORDER BY version DESC";
234 $row = PMA_DBI_fetch_array(PMA_query_as_controluser($sql_query));
236 if (isset($row['tracking_active']) && $row['tracking_active'] == 1) {
244 * Returns the comment line for the log.
246 * @return string Comment, contains date and username
248 static public function getLogComment()
250 $date = date('Y-m-d H:i:s');
252 return "# log " . $date . " " . $GLOBALS['cfg']['Server']['user'] . "\n";
256 * Creates tracking version of a table / view
257 * (in other words: create a job to track future changes on the table).
261 * @param string $dbname name of database
262 * @param string $tablename name of table
263 * @param string $version version
264 * @param string $tracking_set set of tracking statements
265 * @param string $is_view if table is a view
267 * @return int result of version insertion
269 static public function createVersion($dbname, $tablename, $version, $tracking_set = '', $is_view = false)
271 global $sql_backquotes;
273 if ($tracking_set == '') {
274 $tracking_set = self
::$default_tracking_set;
277 require_once './libraries/export/sql.php';
279 $sql_backquotes = true;
281 $date = date('Y-m-d H:i:s');
283 // Get data definition snapshot of table
285 SHOW FULL COLUMNS FROM ' . PMA_backquote($dbname) . '.' . PMA_backquote($tablename);
287 $sql_result = PMA_DBI_query($sql_query);
289 while ($row = PMA_DBI_fetch_array($sql_result)) {
294 SHOW INDEX FROM ' . PMA_backquote($dbname) . '.' . PMA_backquote($tablename);
296 $sql_result = PMA_DBI_query($sql_query);
300 while($row = PMA_DBI_fetch_array($sql_result)) {
304 $snapshot = array('COLUMNS' => $columns, 'INDEXES' => $indexes);
305 $snapshot = serialize($snapshot);
307 // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements
308 $sql_backquotes = true;
312 if (self
::$add_drop_table == true && $is_view == false) {
313 $create_sql .= self
::getLogComment() .
314 self
::getStatementDropTable(PMA_backquote($tablename)) . ";\n";
318 if (self
::$add_drop_view == true && $is_view == true) {
319 $create_sql .= self
::getLogComment() .
320 self
::getStatementDropView(PMA_backquote($tablename)) . ";\n";
323 $create_sql .= self
::getLogComment() .
324 PMA_getTableDef($dbname, $tablename, "\n", "");
330 "INSERT INTO" . self
::$pma_table . " (" .
336 "schema_snapshot, " .
342 '" . PMA_sqlAddslashes($dbname) . "',
343 '" . PMA_sqlAddslashes($tablename) . "',
344 '" . PMA_sqlAddslashes($version) . "',
345 '" . PMA_sqlAddslashes($date) . "',
346 '" . PMA_sqlAddslashes($date) . "',
347 '" . PMA_sqlAddslashes($snapshot) . "',
348 '" . PMA_sqlAddslashes($create_sql) . "',
349 '" . PMA_sqlAddslashes("\n") . "',
350 '" . PMA_sqlAddslashes($tracking_set) . "' )";
352 $result = PMA_query_as_controluser($sql_query);
355 // Deactivate previous version
356 self
::deactivateTracking($dbname, $tablename, ($version - 1));
364 * Removes all tracking data for a table
368 * @param string $dbname name of database
369 * @param string $tablename name of table
371 * @return int result of version insertion
373 static public function deleteTracking($dbname, $tablename)
377 "DELETE FROM " . self
::$pma_table . " WHERE `db_name` = '" . PMA_sqlAddslashes($dbname) . "' AND `table_name` = '" . PMA_sqlAddslashes($tablename) . "'";
378 $result = PMA_query_as_controluser($sql_query);
384 * Creates tracking version of a database
385 * (in other words: create a job to track future changes on the database).
389 * @param string $dbname name of database
390 * @param string $version version
391 * @param string $query query
392 * @param string $tracking_set set of tracking statements
394 * @return int result of version insertion
396 static public function createDatabaseVersion($dbname, $version, $query, $tracking_set = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE')
398 global $sql_backquotes;
400 $date = date('Y-m-d H:i:s');
402 if ($tracking_set == '') {
403 $tracking_set = self
::$default_tracking_set;
406 require_once './libraries/export/sql.php';
410 if (self
::$add_drop_database == true) {
411 $create_sql .= self
::getLogComment() .
412 self
::getStatementDropDatabase(PMA_backquote($dbname)) . ";\n";
415 $create_sql .= self
::getLogComment() . $query;
420 "INSERT INTO" . self
::$pma_table . " (" .
426 "schema_snapshot, " .
432 '" . PMA_sqlAddslashes($dbname) . "',
433 '" . PMA_sqlAddslashes('') . "',
434 '" . PMA_sqlAddslashes($version) . "',
435 '" . PMA_sqlAddslashes($date) . "',
436 '" . PMA_sqlAddslashes($date) . "',
437 '" . PMA_sqlAddslashes('') . "',
438 '" . PMA_sqlAddslashes($create_sql) . "',
439 '" . PMA_sqlAddslashes("\n") . "',
440 '" . PMA_sqlAddslashes($tracking_set) . "' )";
442 $result = PMA_query_as_controluser($sql_query);
450 * Changes tracking of a table.
454 * @param string $dbname name of database
455 * @param string $tablename name of table
456 * @param string $version version
457 * @param integer $new_state the new state of tracking
459 * @return int result of SQL query
461 static private function changeTracking($dbname, $tablename, $version, $new_state)
464 " UPDATE " . self
::$pma_table .
465 " SET `tracking_active` = '" . $new_state . "' " .
466 " WHERE `db_name` = '" . PMA_sqlAddslashes($dbname) . "' " .
467 " AND `table_name` = '" . PMA_sqlAddslashes($tablename) . "' " .
468 " AND `version` = '" . PMA_sqlAddslashes($version) . "' ";
470 $result = PMA_query_as_controluser($sql_query);
476 * Activates tracking of a table.
480 * @param string $dbname name of database
481 * @param string $tablename name of table
482 * @param string $version version
484 * @return int result of SQL query
486 static public function activateTracking($dbname, $tablename, $version)
488 return self
::changeTracking($dbname, $tablename, $version, 1);
493 * Deactivates tracking of a table.
497 * @param string $dbname name of database
498 * @param string $tablename name of table
499 * @param string $version version
501 * @return int result of SQL query
503 static public function deactivateTracking($dbname, $tablename, $version)
505 return self
::changeTracking($dbname, $tablename, $version, 0);
510 * Gets the newest version of a tracking job
511 * (in other words: gets the HEAD version).
515 * @param string $dbname name of database
516 * @param string $tablename name of table
517 * @param string $statement tracked statement
519 * @return int (-1 if no version exists | > 0 if a version exists)
521 static public function getVersion($dbname, $tablename, $statement = null)
524 " SELECT MAX(version) FROM " . self
::$pma_table .
525 " WHERE `db_name` = '" . PMA_sqlAddslashes($dbname) . "' " .
526 " AND `table_name` = '" . PMA_sqlAddslashes($tablename) . "' ";
528 if ($statement != "") {
529 $sql_query .= " AND FIND_IN_SET('" . $statement . "',tracking) > 0" ;
531 $row = PMA_DBI_fetch_array(PMA_query_as_controluser($sql_query));
532 if (isset($row[0])) {
535 if (! isset($version)) {
543 * Gets the record of a tracking job.
547 * @param string $dbname name of database
548 * @param string $tablename name of table
549 * @param string $version version number
551 * @return mixed record DDM log, DDL log, structure snapshot, tracked statements.
553 static public function getTrackedData($dbname, $tablename, $version)
555 if (! isset(self
::$pma_table)) {
558 $sql_query = " SELECT * FROM " . self
::$pma_table .
559 " WHERE `db_name` = '" . PMA_sqlAddslashes($dbname) . "' ";
560 if (! empty($tablename)) {
561 $sql_query .= " AND `table_name` = '" . PMA_sqlAddslashes($tablename) ."' ";
563 $sql_query .= " AND `version` = '" . PMA_sqlAddslashes($version) ."' ".
564 " ORDER BY `version` DESC ";
566 $mixed = PMA_DBI_fetch_array(PMA_query_as_controluser($sql_query));
569 $log_schema_entries = explode('# log ', $mixed['schema_sql']);
570 $log_data_entries = explode('# log ', $mixed['data_sql']);
572 $ddl_date_from = $date = date('Y-m-d H:i:s');
577 // Iterate tracked data definition statements
578 // For each log entry we want to get date, username and statement
579 foreach ($log_schema_entries as $log_entry) {
580 if (trim($log_entry) != '') {
581 $date = substr($log_entry, 0, 19);
582 $username = substr($log_entry, 20, strpos($log_entry, "\n") - 20);
584 $ddl_date_from = $date;
586 $statement = rtrim(strstr($log_entry, "\n"));
588 $ddlog[] = array( 'date' => $date,
589 'username'=> $username,
590 'statement' => $statement );
595 $date_from = $ddl_date_from;
596 $date_to = $ddl_date_to = $date;
598 $dml_date_from = $date_from;
603 // Iterate tracked data manipulation statements
604 // For each log entry we want to get date, username and statement
605 foreach ($log_data_entries as $log_entry) {
606 if (trim($log_entry) != '') {
607 $date = substr($log_entry, 0, 19);
608 $username = substr($log_entry, 20, strpos($log_entry, "\n") - 20);
610 $dml_date_from = $date;
612 $statement = rtrim(strstr($log_entry, "\n"));
614 $dmlog[] = array( 'date' => $date,
615 'username' => $username,
616 'statement' => $statement );
621 $dml_date_to = $date;
623 // Define begin and end of date range for both logs
624 if (strtotime($ddl_date_from) <= strtotime($dml_date_from)) {
625 $data['date_from'] = $ddl_date_from;
627 $data['date_from'] = $dml_date_from;
629 if (strtotime($ddl_date_to) >= strtotime($dml_date_to)) {
630 $data['date_to'] = $ddl_date_to;
632 $data['date_to'] = $dml_date_to;
634 $data['ddlog'] = $ddlog;
635 $data['dmlog'] = $dmlog;
636 $data['tracking'] = $mixed['tracking'];
637 $data['schema_snapshot'] = $mixed['schema_snapshot'];
644 * Parses a query. Gets
645 * - statement identifier (UPDATE, ALTER TABLE, ...)
646 * - type of statement, is it part of DDL or DML ?
650 * @todo: using PMA SQL Parser when possible
651 * @todo: support multi-table/view drops
653 * @param string $query
655 * @return mixed Array containing identifier, type and tablename.
658 static public function parseQuery($query)
661 // Usage of PMA_SQP does not work here
663 // require_once("libraries/sqlparser.lib.php");
664 // $parsed_sql = PMA_SQP_parse($query);
665 // $sql_info = PMA_SQP_analyze($parsed_sql);
667 $query = str_replace("\n", " ", $query);
668 $query = str_replace("\r", " ", $query);
670 $query = trim($query);
671 $query = trim($query, ' -');
673 $tokens = explode(" ", $query);
674 $tokens = array_map('strtoupper', $tokens);
676 // Parse USE statement, need it for SQL dump imports
677 if (substr($query, 0, 4) == 'USE ') {
678 $prefix = explode('USE ', $query);
679 $GLOBALS['db'] = self
::getTableName($prefix[1]);
686 $result['type'] = 'DDL';
688 // Parse CREATE VIEW statement
689 if (in_array('CREATE', $tokens) == true &&
690 in_array('VIEW', $tokens) == true &&
691 in_array('AS', $tokens) == true) {
692 $result['identifier'] = 'CREATE VIEW';
694 $index = array_search('VIEW', $tokens);
696 $result['tablename'] = strtolower(self
::getTableName($tokens[$index +
1]));
699 // Parse ALTER VIEW statement
700 if (in_array('ALTER', $tokens) == true &&
701 in_array('VIEW', $tokens) == true &&
702 in_array('AS', $tokens) == true &&
703 ! isset($result['identifier'])) {
704 $result['identifier'] = 'ALTER VIEW';
706 $index = array_search('VIEW', $tokens);
708 $result['tablename'] = strtolower(self
::getTableName($tokens[$index +
1]));
711 // Parse DROP VIEW statement
712 if (! isset($result['identifier']) && substr($query, 0, 10) == 'DROP VIEW ') {
713 $result['identifier'] = 'DROP VIEW';
715 $prefix = explode('DROP VIEW ', $query);
716 $str = strstr($prefix[1], 'IF EXISTS');
718 if ($str == FALSE ) {
721 $result['tablename'] = self
::getTableName($str);
724 // Parse CREATE DATABASE statement
725 if (! isset($result['identifier']) && substr($query, 0, 15) == 'CREATE DATABASE') {
726 $result['identifier'] = 'CREATE DATABASE';
727 $str = str_replace('CREATE DATABASE', '', $query);
728 $str = str_replace('IF NOT EXISTS', '', $str);
730 $prefix = explode('DEFAULT ', $str);
732 $result['tablename'] = '';
733 $GLOBALS['db'] = self
::getTableName($prefix[0]);
736 // Parse ALTER DATABASE statement
737 if (! isset($result['identifier']) && substr($query, 0, 14) == 'ALTER DATABASE') {
738 $result['identifier'] = 'ALTER DATABASE';
739 $result['tablename'] = '';
742 // Parse DROP DATABASE statement
743 if (! isset($result['identifier']) && substr($query, 0, 13) == 'DROP DATABASE') {
744 $result['identifier'] = 'DROP DATABASE';
745 $str = str_replace('DROP DATABASE', '', $query);
746 $str = str_replace('IF EXISTS', '', $str);
747 $GLOBALS['db'] = self
::getTableName($str);
748 $result['tablename'] = '';
751 // Parse CREATE TABLE statement
752 if (! isset($result['identifier']) && substr($query, 0, 12) == 'CREATE TABLE' ) {
753 $result['identifier'] = 'CREATE TABLE';
754 $query = str_replace('IF NOT EXISTS', '', $query);
755 $prefix = explode('CREATE TABLE ', $query);
756 $suffix = explode('(', $prefix[1]);
757 $result['tablename'] = self
::getTableName($suffix[0]);
760 // Parse ALTER TABLE statement
761 if (! isset($result['identifier']) && substr($query, 0, 12) == 'ALTER TABLE ') {
762 $result['identifier'] = 'ALTER TABLE';
764 $prefix = explode('ALTER TABLE ', $query);
765 $suffix = explode(' ', $prefix[1]);
766 $result['tablename'] = self
::getTableName($suffix[0]);
769 // Parse DROP TABLE statement
770 if (! isset($result['identifier']) && substr($query, 0, 11) == 'DROP TABLE ') {
771 $result['identifier'] = 'DROP TABLE';
773 $prefix = explode('DROP TABLE ', $query);
774 $str = strstr($prefix[1], 'IF EXISTS');
776 if ($str == FALSE ) {
779 $result['tablename'] = self
::getTableName($str);
782 // Parse CREATE INDEX statement
783 if (! isset($result['identifier']) &&
784 ( substr($query, 0, 12) == 'CREATE INDEX' ||
785 substr($query, 0, 19) == 'CREATE UNIQUE INDEX' ||
786 substr($query, 0, 20) == 'CREATE SPATIAL INDEX'
789 $result['identifier'] = 'CREATE INDEX';
790 $prefix = explode('ON ', $query);
791 $suffix = explode('(', $prefix[1]);
792 $result['tablename'] = self
::getTableName($suffix[0]);
795 // Parse DROP INDEX statement
796 if (! isset($result['identifier']) && substr($query, 0, 10) == 'DROP INDEX') {
797 $result['identifier'] = 'DROP INDEX';
798 $prefix = explode('ON ', $query);
799 $result['tablename'] = self
::getTableName($prefix[1]);
802 // Parse RENAME TABLE statement
803 if (! isset($result['identifier']) && substr($query, 0, 13) == 'RENAME TABLE ') {
804 $result['identifier'] = 'RENAME TABLE';
805 $prefix = explode('RENAME TABLE ', $query);
806 $names = explode(' TO ', $prefix[1]);
807 $result['tablename'] = self
::getTableName($names[0]);
808 $result["tablename_after_rename"] = self
::getTableName($names[1]);
815 if (! isset($result['identifier'])) {
816 $result["type"] = 'DML';
818 // Parse UPDATE statement
819 if (! isset($result['identifier']) && substr($query, 0, 6) == 'UPDATE') {
820 $result['identifier'] = 'UPDATE';
821 $prefix = explode('UPDATE ', $query);
822 $suffix = explode(' ', $prefix[1]);
823 $result['tablename'] = self
::getTableName($suffix[0]);
826 // Parse INSERT INTO statement
827 if (! isset($result['identifier']) && substr($query, 0, 11 ) == 'INSERT INTO') {
828 $result['identifier'] = 'INSERT';
829 $prefix = explode('INSERT INTO', $query);
830 $suffix = explode('(', $prefix[1]);
831 $result['tablename'] = self
::getTableName($suffix[0]);
834 // Parse DELETE statement
835 if (! isset($result['identifier']) && substr($query, 0, 6 ) == 'DELETE') {
836 $result['identifier'] = 'DELETE';
837 $prefix = explode('FROM ', $query);
838 $suffix = explode(' ', $prefix[1]);
839 $result['tablename'] = self
::getTableName($suffix[0]);
842 // Parse TRUNCATE statement
843 if (! isset($result['identifier']) && substr($query, 0, 8 ) == 'TRUNCATE') {
844 $result['identifier'] = 'TRUNCATE';
845 $prefix = explode('TRUNCATE', $query);
846 $result['tablename'] = self
::getTableName($prefix[1]);
854 * Analyzes a given SQL statement and saves tracking data.
858 * @param string $query a SQL query
860 static public function handleQuery($query)
862 // If query is marked as untouchable, leave
863 if (strstr($query, "/*NOTRACK*/")) {
867 if (! (substr($query, -1) == ';')) {
868 $query = $query . ";\n";
870 // Get some information about query
871 $result = self
::parseQuery($query);
874 $dbname = trim($GLOBALS['db'], '`');
875 // $dbname can be empty, for example when coming from Synchronize
876 // and this is a query for the remote server
877 if (empty($dbname)) {
881 // If we found a valid statement
882 if (isset($result['identifier'])) {
883 $version = self
::getVersion($dbname, $result['tablename'], $result['identifier']);
885 // If version not exists and auto-creation is enabled
886 if (self
::$version_auto_create == true
887 && self
::isTracked($dbname, $result['tablename']) == false
889 // Create the version
891 switch ($result['identifier']) {
893 self
::createVersion($dbname, $result['tablename'], '1');
896 self
::createVersion($dbname, $result['tablename'], '1', '', true);
898 case 'CREATE DATABASE':
899 self
::createDatabaseVersion($dbname, '1', $query);
905 if (self
::isTracked($dbname, $result['tablename']) && $version != -1) {
906 if ($result['type'] == 'DDL') {
907 $save_to = 'schema_sql';
908 } elseif ($result['type'] == 'DML') {
909 $save_to = 'data_sql';
913 $date = date('Y-m-d H:i:s');
915 // Cut off `dbname`. from query
916 $query = preg_replace('/`' . $dbname . '`\s?\./', '', $query);
918 // Add log information
919 $query = self
::getLogComment() . $query ;
921 // Mark it as untouchable
924 " UPDATE " . self
::$pma_table .
925 " SET " . PMA_backquote($save_to) ." = CONCAT( " . PMA_backquote($save_to) . ",'\n" . PMA_sqlAddslashes($query) . "') ," .
926 " `date_updated` = '" . $date . "' ";
928 // If table was renamed we have to change the tablename attribute in pma_tracking too
929 if ($result['identifier'] == 'RENAME TABLE') {
930 $sql_query .= ', `table_name` = \'' . PMA_sqlAddslashes($result['tablename_after_rename']) . '\' ';
933 // Save the tracking information only for
935 // 2. the table / view
939 " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" .
940 " AND `db_name` = '" . PMA_sqlAddslashes($dbname) . "' " .
941 " AND `table_name` = '" . PMA_sqlAddslashes($result['tablename']) . "' " .
942 " AND `version` = '" . PMA_sqlAddslashes($version) . "' ";
944 $result = PMA_query_as_controluser($sql_query);