Tracker.php 30 KB

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