Tracker.php 30 KB

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