Tracker.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Tracking changes on databases, tables and views
  5. *
  6. * @package PhpMyAdmin
  7. */
  8. namespace PhpMyAdmin;
  9. use PhpMyAdmin\DatabaseInterface;
  10. use PhpMyAdmin\Plugins;
  11. use PhpMyAdmin\Plugins\Export\ExportSql;
  12. use PhpMyAdmin\Relation;
  13. use PhpMyAdmin\SqlParser\Parser;
  14. use PhpMyAdmin\SqlParser\Statements\AlterStatement;
  15. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  16. use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
  17. use PhpMyAdmin\SqlParser\Statements\DropStatement;
  18. use PhpMyAdmin\SqlParser\Statements\InsertStatement;
  19. use PhpMyAdmin\SqlParser\Statements\RenameStatement;
  20. use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
  21. use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
  22. use PhpMyAdmin\Util;
  23. /**
  24. * This class tracks changes on databases, tables and views.
  25. *
  26. * @package PhpMyAdmin
  27. *
  28. * @todo use stristr instead of strstr
  29. */
  30. class Tracker
  31. {
  32. /**
  33. * Whether tracking is ready.
  34. */
  35. static protected $enabled = false;
  36. /**
  37. * Cache to avoid quering tracking status multiple times.
  38. */
  39. static protected $_tracking_cache = array();
  40. /**
  41. * Actually enables tracking. This needs to be done after all
  42. * underlaying code is initialized.
  43. *
  44. * @static
  45. *
  46. * @return void
  47. */
  48. public static function enable()
  49. {
  50. self::$enabled = true;
  51. }
  52. /**
  53. * Gets the on/off value of the Tracker module, starts initialization.
  54. *
  55. * @static
  56. *
  57. * @return boolean (true=on|false=off)
  58. */
  59. public static function isActive()
  60. {
  61. if (! self::$enabled) {
  62. return false;
  63. }
  64. /* We need to avoid attempt to track any queries
  65. * from Relation::getRelationsParam
  66. */
  67. self::$enabled = false;
  68. $relation = new Relation();
  69. $cfgRelation = $relation->getRelationsParam();
  70. /* Restore original state */
  71. self::$enabled = true;
  72. if (! $cfgRelation['trackingwork']) {
  73. return false;
  74. }
  75. $pma_table = self::_getTrackingTable();
  76. return isset($pma_table);
  77. }
  78. /**
  79. * Parses the name of a table from a SQL statement substring.
  80. *
  81. * @param string $string part of SQL statement
  82. *
  83. * @static
  84. *
  85. * @return string the name of table
  86. */
  87. static protected function getTableName($string)
  88. {
  89. if (mb_strstr($string, '.')) {
  90. $temp = explode('.', $string);
  91. $tablename = $temp[1];
  92. } else {
  93. $tablename = $string;
  94. }
  95. $str = explode("\n", $tablename);
  96. $tablename = $str[0];
  97. $tablename = str_replace(';', '', $tablename);
  98. $tablename = str_replace('`', '', $tablename);
  99. $tablename = trim($tablename);
  100. return $tablename;
  101. }
  102. /**
  103. * Gets the tracking status of a table, is it active or deactive ?
  104. *
  105. * @param string $dbname name of database
  106. * @param string $tablename name of table
  107. *
  108. * @static
  109. *
  110. * @return boolean true or false
  111. */
  112. public static function isTracked($dbname, $tablename)
  113. {
  114. if (! self::$enabled) {
  115. return false;
  116. }
  117. if (isset(self::$_tracking_cache[$dbname][$tablename])) {
  118. return self::$_tracking_cache[$dbname][$tablename];
  119. }
  120. /* We need to avoid attempt to track any queries
  121. * from Relation::getRelationsParam
  122. */
  123. self::$enabled = false;
  124. $relation = new Relation();
  125. $cfgRelation = $relation->getRelationsParam();
  126. /* Restore original state */
  127. self::$enabled = true;
  128. if (! $cfgRelation['trackingwork']) {
  129. return false;
  130. }
  131. $sql_query = " SELECT tracking_active FROM " . self::_getTrackingTable() .
  132. " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
  133. " AND table_name = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
  134. " ORDER BY version DESC LIMIT 1";
  135. $result = $GLOBALS['dbi']->fetchValue($sql_query, 0, 0, DatabaseInterface::CONNECT_CONTROL) == 1;
  136. self::$_tracking_cache[$dbname][$tablename] = $result;
  137. return $result;
  138. }
  139. /**
  140. * Returns the comment line for the log.
  141. *
  142. * @return string Comment, contains date and username
  143. */
  144. public static function getLogComment()
  145. {
  146. $date = Util::date('Y-m-d H:i:s');
  147. $user = preg_replace('/\s+/', ' ', $GLOBALS['cfg']['Server']['user']);
  148. return "# log " . $date . " " . $user . "\n";
  149. }
  150. /**
  151. * Creates tracking version of a table / view
  152. * (in other words: create a job to track future changes on the table).
  153. *
  154. * @param string $dbname name of database
  155. * @param string $tablename name of table
  156. * @param string $version version
  157. * @param string $tracking_set set of tracking statements
  158. * @param bool $is_view if table is a view
  159. *
  160. * @static
  161. *
  162. * @return int result of version insertion
  163. */
  164. public static function createVersion($dbname, $tablename, $version,
  165. $tracking_set = '', $is_view = false
  166. ) {
  167. global $sql_backquotes, $export_type;
  168. $relation = new Relation();
  169. if ($tracking_set == '') {
  170. $tracking_set
  171. = $GLOBALS['cfg']['Server']['tracking_default_statements'];
  172. }
  173. // get Export SQL instance
  174. /* @var $export_sql_plugin PhpMyAdmin\Plugins\Export\ExportSql */
  175. $export_sql_plugin = Plugins::getPlugin(
  176. "export",
  177. "sql",
  178. 'libraries/classes/Plugins/Export/',
  179. array(
  180. 'export_type' => $export_type,
  181. 'single_table' => false,
  182. )
  183. );
  184. $sql_backquotes = true;
  185. $date = Util::date('Y-m-d H:i:s');
  186. // Get data definition snapshot of table
  187. $columns = $GLOBALS['dbi']->getColumns($dbname, $tablename, null, true);
  188. // int indices to reduce size
  189. $columns = array_values($columns);
  190. // remove Privileges to reduce size
  191. for ($i = 0, $nb = count($columns); $i < $nb; $i++) {
  192. unset($columns[$i]['Privileges']);
  193. }
  194. $indexes = $GLOBALS['dbi']->getTableIndexes($dbname, $tablename);
  195. $snapshot = array('COLUMNS' => $columns, 'INDEXES' => $indexes);
  196. $snapshot = serialize($snapshot);
  197. // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements
  198. $sql_backquotes = true;
  199. $create_sql = "";
  200. if ($GLOBALS['cfg']['Server']['tracking_add_drop_table'] == true
  201. && $is_view == false
  202. ) {
  203. $create_sql .= self::getLogComment()
  204. . 'DROP TABLE IF EXISTS ' . Util::backquote($tablename) . ";\n";
  205. }
  206. if ($GLOBALS['cfg']['Server']['tracking_add_drop_view'] == true
  207. && $is_view == true
  208. ) {
  209. $create_sql .= self::getLogComment()
  210. . 'DROP VIEW IF EXISTS ' . Util::backquote($tablename) . ";\n";
  211. }
  212. $create_sql .= self::getLogComment() .
  213. $export_sql_plugin->getTableDef($dbname, $tablename, "\n", "");
  214. // Save version
  215. $sql_query = "/*NOTRACK*/\n" .
  216. "INSERT INTO " . self::_getTrackingTable() . " (" .
  217. "db_name, " .
  218. "table_name, " .
  219. "version, " .
  220. "date_created, " .
  221. "date_updated, " .
  222. "schema_snapshot, " .
  223. "schema_sql, " .
  224. "data_sql, " .
  225. "tracking " .
  226. ") " .
  227. "values (
  228. '" . $GLOBALS['dbi']->escapeString($dbname) . "',
  229. '" . $GLOBALS['dbi']->escapeString($tablename) . "',
  230. '" . $GLOBALS['dbi']->escapeString($version) . "',
  231. '" . $GLOBALS['dbi']->escapeString($date) . "',
  232. '" . $GLOBALS['dbi']->escapeString($date) . "',
  233. '" . $GLOBALS['dbi']->escapeString($snapshot) . "',
  234. '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
  235. '" . $GLOBALS['dbi']->escapeString("\n") . "',
  236. '" . $GLOBALS['dbi']->escapeString($tracking_set)
  237. . "' )";
  238. $result = $relation->queryAsControlUser($sql_query);
  239. if ($result) {
  240. // Deactivate previous version
  241. self::deactivateTracking($dbname, $tablename, ($version - 1));
  242. }
  243. return $result;
  244. }
  245. /**
  246. * Removes all tracking data for a table or a version of a table
  247. *
  248. * @param string $dbname name of database
  249. * @param string $tablename name of table
  250. * @param string $version version
  251. *
  252. * @static
  253. *
  254. * @return int result of version insertion
  255. */
  256. public static function deleteTracking($dbname, $tablename, $version = '')
  257. {
  258. $relation = new Relation();
  259. $sql_query = "/*NOTRACK*/\n"
  260. . "DELETE FROM " . self::_getTrackingTable()
  261. . " WHERE `db_name` = '"
  262. . $GLOBALS['dbi']->escapeString($dbname) . "'"
  263. . " AND `table_name` = '"
  264. . $GLOBALS['dbi']->escapeString($tablename) . "'";
  265. if ($version) {
  266. $sql_query .= " AND `version` = '"
  267. . $GLOBALS['dbi']->escapeString($version) . "'";
  268. }
  269. $result = $relation->queryAsControlUser($sql_query);
  270. return $result;
  271. }
  272. /**
  273. * Creates tracking version of a database
  274. * (in other words: create a job to track future changes on the database).
  275. *
  276. * @param string $dbname name of database
  277. * @param string $version version
  278. * @param string $query query
  279. * @param string $tracking_set set of tracking statements
  280. *
  281. * @static
  282. *
  283. * @return int result of version insertion
  284. */
  285. public static function createDatabaseVersion($dbname, $version, $query,
  286. $tracking_set = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE'
  287. ) {
  288. $relation = new Relation();
  289. $date = Util::date('Y-m-d H:i:s');
  290. if ($tracking_set == '') {
  291. $tracking_set
  292. = $GLOBALS['cfg']['Server']['tracking_default_statements'];
  293. }
  294. $create_sql = "";
  295. if ($GLOBALS['cfg']['Server']['tracking_add_drop_database'] == true) {
  296. $create_sql .= self::getLogComment()
  297. . 'DROP DATABASE IF EXISTS ' . Util::backquote($dbname) . ";\n";
  298. }
  299. $create_sql .= self::getLogComment() . $query;
  300. // Save version
  301. $sql_query = "/*NOTRACK*/\n" .
  302. "INSERT INTO " . self::_getTrackingTable() . " (" .
  303. "db_name, " .
  304. "table_name, " .
  305. "version, " .
  306. "date_created, " .
  307. "date_updated, " .
  308. "schema_snapshot, " .
  309. "schema_sql, " .
  310. "data_sql, " .
  311. "tracking " .
  312. ") " .
  313. "values (
  314. '" . $GLOBALS['dbi']->escapeString($dbname) . "',
  315. '" . $GLOBALS['dbi']->escapeString('') . "',
  316. '" . $GLOBALS['dbi']->escapeString($version) . "',
  317. '" . $GLOBALS['dbi']->escapeString($date) . "',
  318. '" . $GLOBALS['dbi']->escapeString($date) . "',
  319. '" . $GLOBALS['dbi']->escapeString('') . "',
  320. '" . $GLOBALS['dbi']->escapeString($create_sql) . "',
  321. '" . $GLOBALS['dbi']->escapeString("\n") . "',
  322. '" . $GLOBALS['dbi']->escapeString($tracking_set)
  323. . "' )";
  324. $result = $relation->queryAsControlUser($sql_query);
  325. return $result;
  326. }
  327. /**
  328. * Changes tracking of a table.
  329. *
  330. * @param string $dbname name of database
  331. * @param string $tablename name of table
  332. * @param string $version version
  333. * @param integer $new_state the new state of tracking
  334. *
  335. * @static
  336. *
  337. * @return int result of SQL query
  338. */
  339. static private function _changeTracking($dbname, $tablename,
  340. $version, $new_state
  341. ) {
  342. $relation = new Relation();
  343. $sql_query = " UPDATE " . self::_getTrackingTable() .
  344. " SET `tracking_active` = '" . $new_state . "' " .
  345. " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
  346. " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
  347. " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
  348. $result = $relation->queryAsControlUser($sql_query);
  349. return $result;
  350. }
  351. /**
  352. * Changes tracking data of a table.
  353. *
  354. * @param string $dbname name of database
  355. * @param string $tablename name of table
  356. * @param string $version version
  357. * @param string $type type of data(DDL || DML)
  358. * @param string|array $new_data the new tracking data
  359. *
  360. * @static
  361. *
  362. * @return bool result of change
  363. */
  364. public static function changeTrackingData($dbname, $tablename,
  365. $version, $type, $new_data
  366. ) {
  367. $relation = new Relation();
  368. if ($type == 'DDL') {
  369. $save_to = 'schema_sql';
  370. } elseif ($type == 'DML') {
  371. $save_to = 'data_sql';
  372. } else {
  373. return false;
  374. }
  375. $date = Util::date('Y-m-d H:i:s');
  376. $new_data_processed = '';
  377. if (is_array($new_data)) {
  378. foreach ($new_data as $data) {
  379. $new_data_processed .= '# log ' . $date . ' ' . $data['username']
  380. . $GLOBALS['dbi']->escapeString($data['statement']) . "\n";
  381. }
  382. } else {
  383. $new_data_processed = $new_data;
  384. }
  385. $sql_query = " UPDATE " . self::_getTrackingTable() .
  386. " SET `" . $save_to . "` = '" . $new_data_processed . "' " .
  387. " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
  388. " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " .
  389. " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
  390. $result = $relation->queryAsControlUser($sql_query);
  391. return (boolean) $result;
  392. }
  393. /**
  394. * Activates tracking of a table.
  395. *
  396. * @param string $dbname name of database
  397. * @param string $tablename name of table
  398. * @param string $version version
  399. *
  400. * @static
  401. *
  402. * @return int result of SQL query
  403. */
  404. public static function activateTracking($dbname, $tablename, $version)
  405. {
  406. return self::_changeTracking($dbname, $tablename, $version, 1);
  407. }
  408. /**
  409. * Deactivates tracking of a table.
  410. *
  411. * @param string $dbname name of database
  412. * @param string $tablename name of table
  413. * @param string $version version
  414. *
  415. * @static
  416. *
  417. * @return int result of SQL query
  418. */
  419. public static function deactivateTracking($dbname, $tablename, $version)
  420. {
  421. return self::_changeTracking($dbname, $tablename, $version, 0);
  422. }
  423. /**
  424. * Gets the newest version of a tracking job
  425. * (in other words: gets the HEAD version).
  426. *
  427. * @param string $dbname name of database
  428. * @param string $tablename name of table
  429. * @param string $statement tracked statement
  430. *
  431. * @static
  432. *
  433. * @return int (-1 if no version exists | > 0 if a version exists)
  434. */
  435. public static function getVersion($dbname, $tablename, $statement = null)
  436. {
  437. $relation = new Relation();
  438. $sql_query = " SELECT MAX(version) FROM " . self::_getTrackingTable() .
  439. " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
  440. " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' ";
  441. if ($statement != "") {
  442. $sql_query .= " AND FIND_IN_SET('"
  443. . $statement . "',tracking) > 0" ;
  444. }
  445. $row = $GLOBALS['dbi']->fetchArray($relation->queryAsControlUser($sql_query));
  446. return isset($row[0])
  447. ? $row[0]
  448. : -1;
  449. }
  450. /**
  451. * Gets the record of a tracking job.
  452. *
  453. * @param string $dbname name of database
  454. * @param string $tablename name of table
  455. * @param string $version version number
  456. *
  457. * @static
  458. *
  459. * @return mixed record DDM log, DDL log, structure snapshot, tracked
  460. * statements.
  461. */
  462. public static function getTrackedData($dbname, $tablename, $version)
  463. {
  464. $relation = new Relation();
  465. $sql_query = " SELECT * FROM " . self::_getTrackingTable() .
  466. " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' ";
  467. if (! empty($tablename)) {
  468. $sql_query .= " AND `table_name` = '"
  469. . $GLOBALS['dbi']->escapeString($tablename) . "' ";
  470. }
  471. $sql_query .= " AND `version` = '" . $GLOBALS['dbi']->escapeString($version)
  472. . "' " . " ORDER BY `version` DESC LIMIT 1";
  473. $mixed = $GLOBALS['dbi']->fetchAssoc($relation->queryAsControlUser($sql_query));
  474. // PHP 7.4 fix for accessing array offset on null
  475. if (!is_array($mixed)) {
  476. $mixed = array(
  477. 'schema_sql' => null,
  478. 'data_sql' => null,
  479. 'tracking' => null,
  480. 'schema_snapshot' => null,
  481. );
  482. }
  483. // Parse log
  484. $log_schema_entries = explode('# log ', $mixed['schema_sql']);
  485. $log_data_entries = explode('# log ', $mixed['data_sql']);
  486. $ddl_date_from = $date = Util::date('Y-m-d H:i:s');
  487. $ddlog = array();
  488. $first_iteration = true;
  489. // Iterate tracked data definition statements
  490. // For each log entry we want to get date, username and statement
  491. foreach ($log_schema_entries as $log_entry) {
  492. if (trim($log_entry) != '') {
  493. $date = mb_substr($log_entry, 0, 19);
  494. $username = mb_substr(
  495. $log_entry, 20, mb_strpos($log_entry, "\n") - 20
  496. );
  497. if ($first_iteration) {
  498. $ddl_date_from = $date;
  499. $first_iteration = false;
  500. }
  501. $statement = rtrim(mb_strstr($log_entry, "\n"));
  502. $ddlog[] = array( 'date' => $date,
  503. 'username'=> $username,
  504. 'statement' => $statement );
  505. }
  506. }
  507. $date_from = $ddl_date_from;
  508. $ddl_date_to = $date;
  509. $dml_date_from = $date_from;
  510. $dmlog = array();
  511. $first_iteration = true;
  512. // Iterate tracked data manipulation statements
  513. // For each log entry we want to get date, username and statement
  514. foreach ($log_data_entries as $log_entry) {
  515. if (trim($log_entry) != '') {
  516. $date = mb_substr($log_entry, 0, 19);
  517. $username = mb_substr(
  518. $log_entry, 20, mb_strpos($log_entry, "\n") - 20
  519. );
  520. if ($first_iteration) {
  521. $dml_date_from = $date;
  522. $first_iteration = false;
  523. }
  524. $statement = rtrim(mb_strstr($log_entry, "\n"));
  525. $dmlog[] = array( 'date' => $date,
  526. 'username' => $username,
  527. 'statement' => $statement );
  528. }
  529. }
  530. $dml_date_to = $date;
  531. // Define begin and end of date range for both logs
  532. $data = array();
  533. if (strtotime($ddl_date_from) <= strtotime($dml_date_from)) {
  534. $data['date_from'] = $ddl_date_from;
  535. } else {
  536. $data['date_from'] = $dml_date_from;
  537. }
  538. if (strtotime($ddl_date_to) >= strtotime($dml_date_to)) {
  539. $data['date_to'] = $ddl_date_to;
  540. } else {
  541. $data['date_to'] = $dml_date_to;
  542. }
  543. $data['ddlog'] = $ddlog;
  544. $data['dmlog'] = $dmlog;
  545. $data['tracking'] = $mixed['tracking'];
  546. $data['schema_snapshot'] = $mixed['schema_snapshot'];
  547. return $data;
  548. }
  549. /**
  550. * Parses a query. Gets
  551. * - statement identifier (UPDATE, ALTER TABLE, ...)
  552. * - type of statement, is it part of DDL or DML ?
  553. * - tablename
  554. *
  555. * @param string $query query
  556. *
  557. * @static
  558. * @todo: using PMA SQL Parser when possible
  559. * @todo: support multi-table/view drops
  560. *
  561. * @return mixed Array containing identifier, type and tablename.
  562. *
  563. */
  564. public static function parseQuery($query)
  565. {
  566. // Usage of PMA_SQP does not work here
  567. //
  568. // require_once("libraries/sqlparser.lib.php");
  569. // $parsed_sql = PMA_SQP_parse($query);
  570. // $sql_info = PMA_SQP_analyze($parsed_sql);
  571. $parser = new Parser($query);
  572. $tokens = $parser->list->tokens;
  573. // Parse USE statement, need it for SQL dump imports
  574. if ($tokens[0]->value == 'USE') {
  575. $GLOBALS['db'] = $tokens[2]->value;
  576. }
  577. $result = array();
  578. if (!empty($parser->statements)) {
  579. $statement = $parser->statements[0];
  580. $options = isset($statement->options) ? $statement->options->options : null;
  581. /*
  582. * DDL statements
  583. */
  584. $result['type'] = 'DDL';
  585. // Parse CREATE statement
  586. if ($statement instanceof CreateStatement) {
  587. if (empty($options) || !isset($options[6])) {
  588. return $result;
  589. }
  590. if ($options[6] == 'VIEW' || $options[6] == 'TABLE') {
  591. $result['identifier'] = 'CREATE ' . $options[6];
  592. $result['tablename'] = $statement->name->table ;
  593. } elseif ($options[6] == 'DATABASE') {
  594. $result['identifier'] = 'CREATE DATABASE' ;
  595. $result['tablename'] = '' ;
  596. // In case of CREATE DATABASE, table field of the CreateStatement is actually name of the database
  597. $GLOBALS['db'] = $statement->name->database;
  598. } elseif ($options[6] == 'INDEX'
  599. || $options[6] == 'UNIQUE INDEX'
  600. || $options[6] == 'FULLTEXT INDEX'
  601. || $options[6] == 'SPATIAL INDEX'
  602. ){
  603. $result['identifier'] = 'CREATE INDEX';
  604. // In case of CREATE INDEX, we have to get the table name from body of the statement
  605. $result['tablename'] = $statement->body[3]->value == '.' ? $statement->body[4]->value
  606. : $statement->body[2]->value ;
  607. }
  608. }
  609. // Parse ALTER statement
  610. elseif ($statement instanceof AlterStatement) {
  611. if (empty($options) || !isset($options[3])) {
  612. return $result;
  613. }
  614. if ($options[3] == 'VIEW' || $options[3] == 'TABLE') {
  615. $result['identifier'] = 'ALTER ' . $options[3] ;
  616. $result['tablename'] = $statement->table->table ;
  617. } elseif ($options[3] == 'DATABASE') {
  618. $result['identifier'] = 'ALTER DATABASE' ;
  619. $result['tablename'] = '' ;
  620. $GLOBALS['db'] = $statement->table->table ;
  621. }
  622. }
  623. // Parse DROP statement
  624. elseif ($statement instanceof DropStatement) {
  625. if (empty($options) || !isset($options[1])) {
  626. return $result;
  627. }
  628. if ($options[1] == 'VIEW' || $options[1] == 'TABLE') {
  629. $result['identifier'] = 'DROP ' . $options[1] ;
  630. $result['tablename'] = $statement->fields[0]->table;
  631. } elseif ($options[1] == 'DATABASE') {
  632. $result['identifier'] = 'DROP DATABASE' ;
  633. $result['tablename'] = '';
  634. $GLOBALS['db'] = $statement->fields[0]->table;
  635. } elseif ($options[1] == 'INDEX') {
  636. $result['identifier'] = 'DROP INDEX' ;
  637. $result['tablename'] = $statement->table->table;
  638. }
  639. }
  640. // Prase RENAME statement
  641. elseif ($statement instanceof RenameStatement) {
  642. $result['identifier'] = 'RENAME TABLE';
  643. $result['tablename'] = $statement->renames[0]->old->table;
  644. $result['tablename_after_rename'] = $statement->renames[0]->new->table;
  645. }
  646. if (isset($result['identifier'])) {
  647. return $result ;
  648. }
  649. /*
  650. * DML statements
  651. */
  652. $result['type'] = 'DML';
  653. // Parse UPDATE statement
  654. if ($statement instanceof UpdateStatement) {
  655. $result['identifier'] = 'UPDATE';
  656. $result['tablename'] = $statement->tables[0]->table;
  657. }
  658. // Parse INSERT INTO statement
  659. if ($statement instanceof InsertStatement) {
  660. $result['identifier'] = 'INSERT';
  661. $result['tablename'] = $statement->into->dest->table;
  662. }
  663. // Parse DELETE statement
  664. if ($statement instanceof DeleteStatement) {
  665. $result['identifier'] = 'DELETE';
  666. $result['tablename'] = $statement->from[0]->table;
  667. }
  668. // Parse TRUNCATE statement
  669. if ($statement instanceof TruncateStatement) {
  670. $result['identifier'] = 'TRUNCATE' ;
  671. $result['tablename'] = $statement->table->table;
  672. }
  673. }
  674. return $result;
  675. }
  676. /**
  677. * Analyzes a given SQL statement and saves tracking data.
  678. *
  679. * @param string $query a SQL query
  680. *
  681. * @static
  682. *
  683. * @return void
  684. */
  685. public static function handleQuery($query)
  686. {
  687. $relation = new Relation();
  688. // If query is marked as untouchable, leave
  689. if (mb_strstr($query, "/*NOTRACK*/")) {
  690. return;
  691. }
  692. if (! (substr($query, -1) == ';')) {
  693. $query = $query . ";\n";
  694. }
  695. // Get some information about query
  696. $result = self::parseQuery($query);
  697. // Get database name
  698. $dbname = trim(isset($GLOBALS['db']) ? $GLOBALS['db'] : '', '`');
  699. // $dbname can be empty, for example when coming from Synchronize
  700. // and this is a query for the remote server
  701. if (empty($dbname)) {
  702. return;
  703. }
  704. // If we found a valid statement
  705. if (isset($result['identifier'])) {
  706. if (! self::isTracked($dbname, $result['tablename'])) {
  707. return;
  708. }
  709. $version = self::getVersion(
  710. $dbname, $result['tablename'], $result['identifier']
  711. );
  712. // If version not exists and auto-creation is enabled
  713. if ($GLOBALS['cfg']['Server']['tracking_version_auto_create'] == true
  714. && $version == -1
  715. ) {
  716. // Create the version
  717. switch ($result['identifier']) {
  718. case 'CREATE TABLE':
  719. self::createVersion($dbname, $result['tablename'], '1');
  720. break;
  721. case 'CREATE VIEW':
  722. self::createVersion(
  723. $dbname, $result['tablename'], '1', '', true
  724. );
  725. break;
  726. case 'CREATE DATABASE':
  727. self::createDatabaseVersion($dbname, '1', $query);
  728. break;
  729. } // end switch
  730. }
  731. // If version exists
  732. if ($version != -1) {
  733. if ($result['type'] == 'DDL') {
  734. $save_to = 'schema_sql';
  735. } elseif ($result['type'] == 'DML') {
  736. $save_to = 'data_sql';
  737. } else {
  738. $save_to = '';
  739. }
  740. $date = Util::date('Y-m-d H:i:s');
  741. // Cut off `dbname`. from query
  742. $query = preg_replace(
  743. '/`' . preg_quote($dbname, '/') . '`\s?\./',
  744. '',
  745. $query
  746. );
  747. // Add log information
  748. $query = self::getLogComment() . $query ;
  749. // Mark it as untouchable
  750. $sql_query = " /*NOTRACK*/\n"
  751. . " UPDATE " . self::_getTrackingTable()
  752. . " SET " . Util::backquote($save_to)
  753. . " = CONCAT( " . Util::backquote($save_to) . ",'\n"
  754. . $GLOBALS['dbi']->escapeString($query) . "') ,"
  755. . " `date_updated` = '" . $date . "' ";
  756. // If table was renamed we have to change
  757. // the tablename attribute in pma_tracking too
  758. if ($result['identifier'] == 'RENAME TABLE') {
  759. $sql_query .= ', `table_name` = \''
  760. . $GLOBALS['dbi']->escapeString($result['tablename_after_rename'])
  761. . '\' ';
  762. }
  763. // Save the tracking information only for
  764. // 1. the database
  765. // 2. the table / view
  766. // 3. the statements
  767. // we want to track
  768. $sql_query .=
  769. " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" .
  770. " AND `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " .
  771. " AND `table_name` = '"
  772. . $GLOBALS['dbi']->escapeString($result['tablename']) . "' " .
  773. " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' ";
  774. $relation->queryAsControlUser($sql_query);
  775. }
  776. }
  777. }
  778. /**
  779. * Returns the tracking table
  780. *
  781. * @return string tracking table
  782. */
  783. private static function _getTrackingTable()
  784. {
  785. $relation = new Relation();
  786. $cfgRelation = $relation->getRelationsParam();
  787. return Util::backquote($cfgRelation['db'])
  788. . '.' . Util::backquote($cfgRelation['tracking']);
  789. }
  790. }