2 /* vim: set expandtab sw=4 ts=4 sts=4: */
4 * Tracking changes on databases, tables and views
8 namespace PMA\libraries
;
10 use PMA\libraries\plugins\export\ExportSql
;
13 * This class tracks changes on databases, tables and views.
17 * @todo use stristr instead of strstr
22 * Whether tracking is ready.
24 static protected $enabled = false;
27 * Cache to avoid quering tracking status multiple times.
29 static protected $_tracking_cache = array();
32 * Actually enables tracking. This needs to be done after all
33 * underlaying code is initialized.
39 static public function enable()
41 self
::$enabled = true;
45 * Gets the on/off value of the Tracker module, starts initialization.
49 * @return boolean (true=on|false=off)
51 static public function isActive()
53 if (! self
::$enabled) {
56 /* We need to avoid attempt to track any queries
57 * from PMA_getRelationsParam
59 self
::$enabled = false;
60 $cfgRelation = PMA_getRelationsParam();
61 /* Restore original state */
62 self
::$enabled = true;
63 if (! $cfgRelation['trackingwork']) {
67 $pma_table = self
::_getTrackingTable();
68 if (isset($pma_table)) {
76 * Parses the name of a table from a SQL statement substring.
78 * @param string $string part of SQL statement
82 * @return string the name of table
84 static protected function getTableName($string)
86 if (mb_strstr($string, '.')) {
87 $temp = explode('.', $string);
88 $tablename = $temp[1];
93 $str = explode("\n", $tablename);
96 $tablename = str_replace(';', '', $tablename);
97 $tablename = str_replace('`', '', $tablename);
98 $tablename = trim($tablename);
105 * Gets the tracking status of a table, is it active or deactive ?
107 * @param string $dbname name of database
108 * @param string $tablename name of table
112 * @return boolean true or false
114 static public function isTracked($dbname, $tablename)
116 if (! self
::$enabled) {
120 if (isset(self
::$_tracking_cache[$dbname][$tablename])) {
121 return self
::$_tracking_cache[$dbname][$tablename];
123 /* We need to avoid attempt to track any queries
124 * from PMA_getRelationsParam
126 self
::$enabled = false;
127 $cfgRelation = PMA_getRelationsParam();
128 /* Restore original state */
129 self
::$enabled = true;
130 if (! $cfgRelation['trackingwork']) {
134 $sql_query = " SELECT tracking_active FROM " . self
::_getTrackingTable() .
135 " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
136 " AND table_name = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
137 " ORDER BY version DESC LIMIT 1";
139 $result = $GLOBALS['dbi']->fetchValue($sql_query, 0, 0, $GLOBALS['controllink']) == 1;
141 self
::$_tracking_cache[$dbname][$tablename] = $result;
147 * Returns the comment line for the log.
149 * @return string Comment, contains date and username
151 static public function getLogComment()
153 $date = date('Y-m-d H:i:s');
154 $user = preg_replace('/\s+/', ' ', $GLOBALS['cfg']['Server']['user']);
156 return "# log " . $date . " " . $user . "\n";
160 * Creates tracking version of a table / view
161 * (in other words: create a job to track future changes on the table).
163 * @param string $dbname name of database
164 * @param string $tablename name of table
165 * @param string $version version
166 * @param string $tracking_set set of tracking statements
167 * @param bool $is_view if table is a view
171 * @return int result of version insertion
173 static public function createVersion($dbname, $tablename, $version,
174 $tracking_set = '', $is_view = false
176 global $sql_backquotes, $export_type;
178 if ($tracking_set == '') {
180 = $GLOBALS['cfg']['Server']['tracking_default_statements'];
183 // get Export SQL instance
184 include_once "libraries/plugin_interface.lib.php";
185 /* @var $export_sql_plugin \PMA\libraries\plugins\export\ExportSql */
186 $export_sql_plugin = PMA_getPlugin(
189 'libraries/plugins/export/',
191 'export_type' => $export_type,
192 'single_table' => false,
196 $sql_backquotes = true;
198 $date = date('Y-m-d H:i:s');
200 // Get data definition snapshot of table
202 $columns = $GLOBALS['dbi']->getColumns($dbname, $tablename, null, true);
203 // int indices to reduce size
204 $columns = array_values($columns);
205 // remove Privileges to reduce size
206 for ($i = 0, $nb = count($columns); $i < $nb; $i++
) {
207 unset($columns[$i]['Privileges']);
210 $indexes = $GLOBALS['dbi']->getTableIndexes($dbname, $tablename);
212 $snapshot = array('COLUMNS' => $columns, 'INDEXES' => $indexes);
213 $snapshot = serialize($snapshot);
215 // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements
216 $sql_backquotes = true;
220 if ($GLOBALS['cfg']['Server']['tracking_add_drop_table'] == true
223 $create_sql .= self
::getLogComment()
224 . 'DROP TABLE IF EXISTS ' . Util
::backquote($tablename) . ";\n";
228 if ($GLOBALS['cfg']['Server']['tracking_add_drop_view'] == true
231 $create_sql .= self
::getLogComment()
232 . 'DROP VIEW IF EXISTS ' . Util
::backquote($tablename) . ";\n";
235 $create_sql .= self
::getLogComment() .
236 $export_sql_plugin->getTableDef($dbname, $tablename, "\n", "");
240 $sql_query = "/*NOTRACK*/\n" .
241 "INSERT INTO " . self
::_getTrackingTable() . " (" .
247 "schema_snapshot, " .
253 '" . $GLOBALS['dbi']->escapeString($dbname) . "',
254 '" . $GLOBALS['dbi']->escapeString($tablename) . "',
255 '" . $GLOBALS['dbi']->escapeString($version) . "',
256 '" . $GLOBALS['dbi']->escapeString($date) . "',
257 '" . $GLOBALS['dbi']->escapeString($date) . "',
258 '" . $GLOBALS['dbi']->escapeString($snapshot) . "',
259 '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
260 '" . $GLOBALS['dbi']->escapeString("\n") . "',
261 '" . $GLOBALS['dbi']->escapeString($tracking_set)
264 $result = PMA_queryAsControlUser($sql_query);
267 // Deactivate previous version
268 self
::deactivateTracking($dbname, $tablename, ($version - 1));
276 * Removes all tracking data for a table or a version of a table
278 * @param string $dbname name of database
279 * @param string $tablename name of table
280 * @param string $version version
284 * @return int result of version insertion
286 static public function deleteTracking($dbname, $tablename, $version = '')
288 $sql_query = "/*NOTRACK*/\n"
289 . "DELETE FROM " . self
::_getTrackingTable()
290 . " WHERE `db_name` = '"
291 . $GLOBALS['dbi']->escapeString($dbname) . "'"
292 . " AND `table_name` = '"
293 . $GLOBALS['dbi']->escapeString($tablename) . "'";
295 $sql_query .= " AND `version` = '"
296 . $GLOBALS['dbi']->escapeString($version) . "'";
298 $result = PMA_queryAsControlUser($sql_query);
304 * Creates tracking version of a database
305 * (in other words: create a job to track future changes on the database).
307 * @param string $dbname name of database
308 * @param string $version version
309 * @param string $query query
310 * @param string $tracking_set set of tracking statements
314 * @return int result of version insertion
316 static public function createDatabaseVersion($dbname, $version, $query,
317 $tracking_set = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE'
319 $date = date('Y-m-d H:i:s');
321 if ($tracking_set == '') {
323 = $GLOBALS['cfg']['Server']['tracking_default_statements'];
328 if ($GLOBALS['cfg']['Server']['tracking_add_drop_database'] == true) {
329 $create_sql .= self
::getLogComment()
330 . 'DROP DATABASE IF EXISTS ' . Util
::backquote($dbname) . ";\n";
333 $create_sql .= self
::getLogComment() . $query;
336 $sql_query = "/*NOTRACK*/\n" .
337 "INSERT INTO " . self
::_getTrackingTable() . " (" .
343 "schema_snapshot, " .
349 '" . $GLOBALS['dbi']->escapeString($dbname) . "',
350 '" . $GLOBALS['dbi']->escapeString('') . "',
351 '" . $GLOBALS['dbi']->escapeString($version) . "',
352 '" . $GLOBALS['dbi']->escapeString($date) . "',
353 '" . $GLOBALS['dbi']->escapeString($date) . "',
354 '" . $GLOBALS['dbi']->escapeString('') . "',
355 '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
356 '" . $GLOBALS['dbi']->escapeString("\n") . "',
357 '" . $GLOBALS['dbi']->escapeString($tracking_set)
360 $result = PMA_queryAsControlUser($sql_query);
368 * Changes tracking of a table.
370 * @param string $dbname name of database
371 * @param string $tablename name of table
372 * @param string $version version
373 * @param integer $new_state the new state of tracking
377 * @return int result of SQL query
379 static private function _changeTracking($dbname, $tablename,
383 $sql_query = " UPDATE " . self
::_getTrackingTable() .
384 " SET `tracking_active` = '" . $new_state . "' " .
385 " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
386 " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
387 " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
389 $result = PMA_queryAsControlUser($sql_query);
395 * Changes tracking data of a table.
397 * @param string $dbname name of database
398 * @param string $tablename name of table
399 * @param string $version version
400 * @param string $type type of data(DDL || DML)
401 * @param string|array $new_data the new tracking data
405 * @return bool result of change
407 static public function changeTrackingData($dbname, $tablename,
408 $version, $type, $new_data
410 if ($type == 'DDL') {
411 $save_to = 'schema_sql';
412 } elseif ($type == 'DML') {
413 $save_to = 'data_sql';
417 $date = date('Y-m-d H:i:s');
419 $new_data_processed = '';
420 if (is_array($new_data)) {
421 foreach ($new_data as $data) {
422 $new_data_processed .= '# log ' . $date . ' ' . $data['username']
423 . $GLOBALS['dbi']->escapeString($data['statement']) . "\n";
426 $new_data_processed = $new_data;
429 $sql_query = " UPDATE " . self
::_getTrackingTable() .
430 " SET `" . $save_to . "` = '" . $new_data_processed . "' " .
431 " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
432 " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
433 " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
435 $result = PMA_queryAsControlUser($sql_query);
437 return (boolean
) $result;
441 * Activates tracking of a table.
443 * @param string $dbname name of database
444 * @param string $tablename name of table
445 * @param string $version version
449 * @return int result of SQL query
451 static public function activateTracking($dbname, $tablename, $version)
453 return self
::_changeTracking($dbname, $tablename, $version, 1);
458 * Deactivates tracking of a table.
460 * @param string $dbname name of database
461 * @param string $tablename name of table
462 * @param string $version version
466 * @return int result of SQL query
468 static public function deactivateTracking($dbname, $tablename, $version)
470 return self
::_changeTracking($dbname, $tablename, $version, 0);
475 * Gets the newest version of a tracking job
476 * (in other words: gets the HEAD version).
478 * @param string $dbname name of database
479 * @param string $tablename name of table
480 * @param string $statement tracked statement
484 * @return int (-1 if no version exists | > 0 if a version exists)
486 static public function getVersion($dbname, $tablename, $statement = null)
488 $sql_query = " SELECT MAX(version) FROM " . self
::_getTrackingTable() .
489 " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
490 " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' ";
492 if ($statement != "") {
493 $sql_query .= " AND FIND_IN_SET('"
494 . $statement . "',tracking) > 0" ;
496 $row = $GLOBALS['dbi']->fetchArray(PMA_queryAsControlUser($sql_query));
497 return isset($row[0])
504 * Gets the record of a tracking job.
506 * @param string $dbname name of database
507 * @param string $tablename name of table
508 * @param string $version version number
512 * @return mixed record DDM log, DDL log, structure snapshot, tracked
515 static public function getTrackedData($dbname, $tablename, $version)
517 $sql_query = " SELECT * FROM " . self
::_getTrackingTable() .
518 " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' ";
519 if (! empty($tablename)) {
520 $sql_query .= " AND `table_name` = '"
521 . $GLOBALS['dbi']->escapeString($tablename) . "' ";
523 $sql_query .= " AND `version` = '" . $GLOBALS['dbi']->escapeString($version)
524 . "' " . " ORDER BY `version` DESC LIMIT 1";
526 $mixed = $GLOBALS['dbi']->fetchAssoc(PMA_queryAsControlUser($sql_query));
529 $log_schema_entries = explode('# log ', $mixed['schema_sql']);
530 $log_data_entries = explode('# log ', $mixed['data_sql']);
532 $ddl_date_from = $date = date('Y-m-d H:i:s');
535 $first_iteration = true;
537 // Iterate tracked data definition statements
538 // For each log entry we want to get date, username and statement
539 foreach ($log_schema_entries as $log_entry) {
540 if (trim($log_entry) != '') {
541 $date = mb_substr($log_entry, 0, 19);
542 $username = mb_substr(
543 $log_entry, 20, mb_strpos($log_entry, "\n") - 20
545 if ($first_iteration) {
546 $ddl_date_from = $date;
547 $first_iteration = false;
549 $statement = rtrim(mb_strstr($log_entry, "\n"));
551 $ddlog[] = array( 'date' => $date,
552 'username'=> $username,
553 'statement' => $statement );
557 $date_from = $ddl_date_from;
558 $ddl_date_to = $date;
560 $dml_date_from = $date_from;
563 $first_iteration = true;
565 // Iterate tracked data manipulation statements
566 // For each log entry we want to get date, username and statement
567 foreach ($log_data_entries as $log_entry) {
568 if (trim($log_entry) != '') {
569 $date = mb_substr($log_entry, 0, 19);
570 $username = mb_substr(
571 $log_entry, 20, mb_strpos($log_entry, "\n") - 20
573 if ($first_iteration) {
574 $dml_date_from = $date;
575 $first_iteration = false;
577 $statement = rtrim(mb_strstr($log_entry, "\n"));
579 $dmlog[] = array( 'date' => $date,
580 'username' => $username,
581 'statement' => $statement );
585 $dml_date_to = $date;
587 // Define begin and end of date range for both logs
589 if (strtotime($ddl_date_from) <= strtotime($dml_date_from)) {
590 $data['date_from'] = $ddl_date_from;
592 $data['date_from'] = $dml_date_from;
594 if (strtotime($ddl_date_to) >= strtotime($dml_date_to)) {
595 $data['date_to'] = $ddl_date_to;
597 $data['date_to'] = $dml_date_to;
599 $data['ddlog'] = $ddlog;
600 $data['dmlog'] = $dmlog;
601 $data['tracking'] = $mixed['tracking'];
602 $data['schema_snapshot'] = $mixed['schema_snapshot'];
609 * Parses a query. Gets
610 * - statement identifier (UPDATE, ALTER TABLE, ...)
611 * - type of statement, is it part of DDL or DML ?
614 * @param string $query query
617 * @todo: using PMA SQL Parser when possible
618 * @todo: support multi-table/view drops
620 * @return mixed Array containing identifier, type and tablename.
623 static public function parseQuery($query)
625 // Usage of PMA_SQP does not work here
627 // require_once("libraries/sqlparser.lib.php");
628 // $parsed_sql = PMA_SQP_parse($query);
629 // $sql_info = PMA_SQP_analyze($parsed_sql);
631 $parser = new \PhpMyAdmin\SqlParser\
Parser($query);
633 $tokens = $parser->list->tokens
;
635 // Parse USE statement, need it for SQL dump imports
636 if ($tokens[0]->value
== 'USE') {
637 $GLOBALS['db'] = $tokens[2]->value
;
642 if (!empty($parser->statements
)) {
643 $statement = $parser->statements
[0];
644 $options = isset($statement->options
) ?
$statement->options
->options
: null;
649 $result['type'] = 'DDL';
651 // Parse CREATE statement
652 if ($statement instanceof \PhpMyAdmin\SqlParser\Statements\CreateStatement
) {
653 if (empty($options) ||
!isset($options[6])) {
657 if ($options[6] == 'VIEW' ||
$options[6] == 'TABLE') {
658 $result['identifier'] = 'CREATE ' . $options[6];
659 $result['tablename'] = $statement->name
->table
;
660 } elseif ($options[6] == 'DATABASE') {
661 $result['identifier'] = 'CREATE DATABASE' ;
662 $result['tablename'] = '' ;
664 // In case of CREATE DATABASE, table field of the CreateStatement is actually name of the database
665 $GLOBALS['db'] = $statement->name
->table
;
666 } elseif ($options[6] == 'INDEX'
667 ||
$options[6] == 'UNIQUE INDEX'
668 ||
$options[6] == 'FULLTEXT INDEX'
669 ||
$options[6] == 'SPATIAL INDEX'
671 $result['identifier'] = 'CREATE INDEX';
673 // In case of CREATE INDEX, we have to get the table name from body of the statement
674 $result['tablename'] = $statement->body
[3]->value
== '.' ?
$statement->body
[4]->value
675 : $statement->body
[2]->value
;
679 // Parse ALTER statement
680 elseif ($statement instanceof \PhpMyAdmin\SqlParser\Statements\AlterStatement
) {
681 if (empty($options) ||
!isset($options[3])) {
685 if ($options[3] == 'VIEW' ||
$options[3] == 'TABLE') {
686 $result['identifier'] = 'ALTER ' . $options[3] ;
687 $result['tablename'] = $statement->table
->table
;
688 } elseif ($options[3] == 'DATABASE') {
689 $result['identifier'] = 'ALTER DATABASE' ;
690 $result['tablename'] = '' ;
692 $GLOBALS['db'] = $statement->table
->table
;
696 // Parse DROP statement
697 elseif ($statement instanceof \PhpMyAdmin\SqlParser\Statements\DropStatement
) {
698 if (empty($options) ||
!isset($options[1])) {
702 if ($options[1] == 'VIEW' ||
$options[1] == 'TABLE') {
703 $result['identifier'] = 'DROP ' . $options[1] ;
704 $result['tablename'] = $statement->fields
[0]->table
;
705 } elseif ($options[1] == 'DATABASE') {
706 $result['identifier'] = 'DROP DATABASE' ;
707 $result['tablename'] = '';
709 $GLOBALS['db'] = $statement->fields
[0]->table
;
710 } elseif ($options[1] == 'INDEX') {
711 $result['identifier'] = 'DROP INDEX' ;
712 $result['tablename'] = $statement->table
->table
;
716 // Prase RENAME statement
717 elseif ($statement instanceof \PhpMyAdmin\SqlParser\Statements\RenameStatement
) {
718 $result['identifier'] = 'RENAME TABLE';
719 $result['tablename'] = $statement->renames
[0]->old
->table
;
720 $result['tablename_after_rename'] = $statement->renames
[0]->new->table
;
723 if (isset($result['identifier'])) {
730 $result['type'] = 'DML';
732 // Parse UPDATE statement
733 if ($statement instanceof \PhpMyAdmin\SqlParser\Statements\UpdateStatement
) {
734 $result['identifier'] = 'UPDATE';
735 $result['tablename'] = $statement->tables
[0]->table
;
738 // Parse INSERT INTO statement
739 if ($statement instanceof \PhpMyAdmin\SqlParser\Statements\InsertStatement
) {
740 $result['identifier'] = 'INSERT';
741 $result['tablename'] = $statement->into
->dest
->table
;
744 // Parse DELETE statement
745 if ($statement instanceof \PhpMyAdmin\SqlParser\Statements\DeleteStatement
) {
746 $result['identifier'] = 'DELETE';
747 $result['tablename'] = $statement->from
[0]->table
;
750 // Parse TRUNCATE statement
751 if ($statement instanceof \PhpMyAdmin\SqlParser\Statements\TruncateStatement
) {
752 $result['identifier'] = 'TRUNCATE' ;
753 $result['tablename'] = $statement->table
->table
;
762 * Analyzes a given SQL statement and saves tracking data.
764 * @param string $query a SQL query
770 static public function handleQuery($query)
772 // If query is marked as untouchable, leave
773 if (mb_strstr($query, "/*NOTRACK*/")) {
777 if (! (substr($query, -1) == ';')) {
778 $query = $query . ";\n";
780 // Get some information about query
781 $result = self
::parseQuery($query);
784 $dbname = trim(isset($GLOBALS['db']) ?
$GLOBALS['db'] : '', '`');
785 // $dbname can be empty, for example when coming from Synchronize
786 // and this is a query for the remote server
787 if (empty($dbname)) {
791 // If we found a valid statement
792 if (isset($result['identifier'])) {
793 if (! self
::isTracked($dbname, $result['tablename'])) {
797 $version = self
::getVersion(
798 $dbname, $result['tablename'], $result['identifier']
801 // If version not exists and auto-creation is enabled
802 if ($GLOBALS['cfg']['Server']['tracking_version_auto_create'] == true
805 // Create the version
807 switch ($result['identifier']) {
809 self
::createVersion($dbname, $result['tablename'], '1');
813 $dbname, $result['tablename'], '1', '', true
816 case 'CREATE DATABASE':
817 self
::createDatabaseVersion($dbname, '1', $query);
823 if ($version != -1) {
824 if ($result['type'] == 'DDL') {
825 $save_to = 'schema_sql';
826 } elseif ($result['type'] == 'DML') {
827 $save_to = 'data_sql';
831 $date = date('Y-m-d H:i:s');
833 // Cut off `dbname`. from query
834 $query = preg_replace(
835 '/`' . preg_quote($dbname, '/') . '`\s?\./',
840 // Add log information
841 $query = self
::getLogComment() . $query ;
843 // Mark it as untouchable
844 $sql_query = " /*NOTRACK*/\n"
845 . " UPDATE " . self
::_getTrackingTable()
846 . " SET " . Util
::backquote($save_to)
847 . " = CONCAT( " . Util
::backquote($save_to) . ",'\n"
848 . $GLOBALS['dbi']->escapeString($query) . "') ,"
849 . " `date_updated` = '" . $date . "' ";
851 // If table was renamed we have to change
852 // the tablename attribute in pma_tracking too
853 if ($result['identifier'] == 'RENAME TABLE') {
854 $sql_query .= ', `table_name` = \''
855 . $GLOBALS['dbi']->escapeString($result['tablename_after_rename'])
859 // Save the tracking information only for
861 // 2. the table / view
865 " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" .
866 " AND `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
867 " AND `table_name` = '"
868 . $GLOBALS['dbi']->escapeString($result['tablename']) . "' " .
869 " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
871 PMA_queryAsControlUser($sql_query);
877 * Returns the tracking table
879 * @return string tracking table
881 private static function _getTrackingTable()
883 $cfgRelation = PMA_getRelationsParam();
884 return Util
::backquote($cfgRelation['db'])
885 . '.' . Util
::backquote($cfgRelation['tracking']);