PageRenderTime 53ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/classes/Tracker.php

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