Tracking.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231
  1. <?php
  2. /**
  3. * Functions used for database and table tracking
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\Html\Generator;
  8. use const SORT_ASC;
  9. use function array_key_exists;
  10. use function array_merge;
  11. use function array_multisort;
  12. use function count;
  13. use function date;
  14. use function htmlspecialchars;
  15. use function in_array;
  16. use function ini_set;
  17. use function intval;
  18. use function is_array;
  19. use function is_object;
  20. use function mb_strstr;
  21. use function preg_replace;
  22. use function rtrim;
  23. use function sprintf;
  24. use function strlen;
  25. use function strtotime;
  26. /**
  27. * PhpMyAdmin\Tracking class
  28. */
  29. class Tracking
  30. {
  31. /** @var SqlQueryForm */
  32. private $sqlQueryForm;
  33. /** @var Template */
  34. public $template;
  35. /** @var Relation */
  36. protected $relation;
  37. /**
  38. * @param SqlQueryForm $sqlQueryForm SqlQueryForm instance
  39. * @param Template $template Template instance
  40. * @param Relation $relation Relation instance
  41. */
  42. public function __construct(SqlQueryForm $sqlQueryForm, Template $template, Relation $relation)
  43. {
  44. $this->sqlQueryForm = $sqlQueryForm;
  45. $this->template = $template;
  46. $this->relation = $relation;
  47. }
  48. /**
  49. * Filters tracking entries
  50. *
  51. * @param array $data the entries to filter
  52. * @param string $filter_ts_from "from" date
  53. * @param string $filter_ts_to "to" date
  54. * @param array $filter_users users
  55. *
  56. * @return array filtered entries
  57. */
  58. public function filter(
  59. array $data,
  60. $filter_ts_from,
  61. $filter_ts_to,
  62. array $filter_users
  63. ): array {
  64. $tmp_entries = [];
  65. $id = 0;
  66. foreach ($data as $entry) {
  67. $timestamp = strtotime($entry['date']);
  68. $filtered_user = in_array($entry['username'], $filter_users);
  69. if ($timestamp >= $filter_ts_from
  70. && $timestamp <= $filter_ts_to
  71. && (in_array('*', $filter_users) || $filtered_user)
  72. ) {
  73. $tmp_entries[] = [
  74. 'id' => $id,
  75. 'timestamp' => $timestamp,
  76. 'username' => $entry['username'],
  77. 'statement' => $entry['statement'],
  78. ];
  79. }
  80. $id++;
  81. }
  82. return $tmp_entries;
  83. }
  84. /**
  85. * Function to get the list versions of the table
  86. *
  87. * @return object|false
  88. */
  89. public function getListOfVersionsOfTable()
  90. {
  91. global $dbi, $db, $table;
  92. $cfgRelation = $this->relation->getRelationsParam();
  93. $query = sprintf(
  94. 'SELECT * FROM %s.%s WHERE db_name = \'%s\' AND table_name = \'%s\' ORDER BY version DESC',
  95. Util::backquote($cfgRelation['db']),
  96. Util::backquote($cfgRelation['tracking']),
  97. $dbi->escapeString($db),
  98. $dbi->escapeString($table)
  99. );
  100. return $dbi->query($query, DatabaseInterface::CONNECT_CONTROL, 0, false);
  101. }
  102. /**
  103. * Function to get html for main page parts that do not use $_REQUEST
  104. *
  105. * @param array $urlParams url parameters
  106. * @param string $themeImagePath path to theme's image folder
  107. * @param string $textDir text direction
  108. * @param int $lastVersion last tracking version
  109. *
  110. * @return string
  111. */
  112. public function getHtmlForMainPage(
  113. $urlParams,
  114. $themeImagePath,
  115. $textDir,
  116. $lastVersion = null
  117. ) {
  118. global $dbi;
  119. $selectableTablesSqlResult = $this->getSqlResultForSelectableTables();
  120. $selectableTablesEntries = [];
  121. while ($entry = $dbi->fetchArray($selectableTablesSqlResult)) {
  122. $entry['is_tracked'] = Tracker::isTracked(
  123. $entry['db_name'],
  124. $entry['table_name']
  125. );
  126. $selectableTablesEntries[] = $entry;
  127. }
  128. $selectableTablesNumRows = $dbi->numRows($selectableTablesSqlResult);
  129. $versionSqlResult = $this->getListOfVersionsOfTable();
  130. if ($lastVersion === null && $versionSqlResult !== false) {
  131. $lastVersion = $this->getTableLastVersionNumber($versionSqlResult);
  132. }
  133. $dbi->dataSeek($versionSqlResult, 0);
  134. $versions = [];
  135. while ($version = $dbi->fetchArray($versionSqlResult)) {
  136. $versions[] = $version;
  137. }
  138. $type = $dbi->getTable($GLOBALS['db'], $GLOBALS['table'])
  139. ->isView() ? 'view' : 'table';
  140. return $this->template->render('table/tracking/main', [
  141. 'url_params' => $urlParams,
  142. 'db' => $GLOBALS['db'],
  143. 'table' => $GLOBALS['table'],
  144. 'selectable_tables_num_rows' => $selectableTablesNumRows,
  145. 'selectable_tables_entries' => $selectableTablesEntries,
  146. 'selected_table' => $_POST['table'] ?? null,
  147. 'last_version' => $lastVersion,
  148. 'versions' => $versions,
  149. 'type' => $type,
  150. 'default_statements' => $GLOBALS['cfg']['Server']['tracking_default_statements'],
  151. 'theme_image_path' => $themeImagePath,
  152. 'text_dir' => $textDir,
  153. ]);
  154. }
  155. /**
  156. * Function to get the last version number of a table
  157. *
  158. * @param object $sql_result sql result
  159. *
  160. * @return int
  161. */
  162. public function getTableLastVersionNumber($sql_result)
  163. {
  164. global $dbi;
  165. $maxversion = $dbi->fetchArray($sql_result);
  166. return intval(is_array($maxversion) ? $maxversion['version'] : null);
  167. }
  168. /**
  169. * Function to get sql results for selectable tables
  170. *
  171. * @return mixed
  172. */
  173. public function getSqlResultForSelectableTables()
  174. {
  175. global $dbi;
  176. $relation = $this->relation;
  177. $cfgRelation = $relation->getRelationsParam();
  178. $sql_query = ' SELECT DISTINCT db_name, table_name FROM ' .
  179. Util::backquote($cfgRelation['db']) . '.' .
  180. Util::backquote($cfgRelation['tracking']) .
  181. " WHERE db_name = '" . $dbi->escapeString($GLOBALS['db']) .
  182. "' " .
  183. ' ORDER BY db_name, table_name';
  184. return $relation->queryAsControlUser($sql_query);
  185. }
  186. /**
  187. * Function to get html for tracking report and tracking report export
  188. *
  189. * @param array $data data
  190. * @param array $url_params url params
  191. * @param bool $selection_schema selection schema
  192. * @param bool $selection_data selection data
  193. * @param bool $selection_both selection both
  194. * @param int $filter_ts_to filter time stamp from
  195. * @param int $filter_ts_from filter time stamp tp
  196. * @param array $filter_users filter users
  197. *
  198. * @return string
  199. */
  200. public function getHtmlForTrackingReport(
  201. array $data,
  202. array $url_params,
  203. $selection_schema,
  204. $selection_data,
  205. $selection_both,
  206. $filter_ts_to,
  207. $filter_ts_from,
  208. array $filter_users
  209. ) {
  210. $html = '<h3>' . __('Tracking report')
  211. . ' [<a href="' . Url::getFromRoute('/table/tracking', $url_params) . '">' . __('Close')
  212. . '</a>]</h3>';
  213. $html .= '<small>' . __('Tracking statements') . ' '
  214. . htmlspecialchars($data['tracking']) . '</small><br>';
  215. $html .= '<br>';
  216. [$str1, $str2, $str3, $str4, $str5] = $this->getHtmlForElementsOfTrackingReport(
  217. $selection_schema,
  218. $selection_data,
  219. $selection_both
  220. );
  221. // Prepare delete link content here
  222. $drop_image_or_text = '';
  223. if (Util::showIcons('ActionLinksMode')) {
  224. $drop_image_or_text .= Generator::getImage(
  225. 'b_drop',
  226. __('Delete tracking data row from report')
  227. );
  228. }
  229. if (Util::showText('ActionLinksMode')) {
  230. $drop_image_or_text .= __('Delete');
  231. }
  232. /*
  233. * First, list tracked data definition statements
  234. */
  235. if (count($data['ddlog']) == 0 && count($data['dmlog']) === 0) {
  236. $msg = Message::notice(__('No data'));
  237. echo $msg->getDisplay();
  238. }
  239. $html .= $this->getHtmlForTrackingReportExportForm1(
  240. $data,
  241. $url_params,
  242. $selection_schema,
  243. $selection_data,
  244. $selection_both,
  245. $filter_ts_to,
  246. $filter_ts_from,
  247. $filter_users,
  248. $str1,
  249. $str2,
  250. $str3,
  251. $str4,
  252. $str5,
  253. $drop_image_or_text
  254. );
  255. $html .= $this->getHtmlForTrackingReportExportForm2(
  256. $url_params,
  257. $str1,
  258. $str2,
  259. $str3,
  260. $str4,
  261. $str5
  262. );
  263. $html .= "<br><br><hr><br>\n";
  264. return $html;
  265. }
  266. /**
  267. * Generate HTML element for report form
  268. *
  269. * @param bool $selection_schema selection schema
  270. * @param bool $selection_data selection data
  271. * @param bool $selection_both selection both
  272. *
  273. * @return array
  274. */
  275. public function getHtmlForElementsOfTrackingReport(
  276. $selection_schema,
  277. $selection_data,
  278. $selection_both
  279. ) {
  280. $str1 = '<select name="logtype">'
  281. . '<option value="schema"'
  282. . ($selection_schema ? ' selected="selected"' : '') . '>'
  283. . __('Structure only') . '</option>'
  284. . '<option value="data"'
  285. . ($selection_data ? ' selected="selected"' : '') . '>'
  286. . __('Data only') . '</option>'
  287. . '<option value="schema_and_data"'
  288. . ($selection_both ? ' selected="selected"' : '') . '>'
  289. . __('Structure and data') . '</option>'
  290. . '</select>';
  291. $str2 = '<input type="text" name="date_from" value="'
  292. . htmlspecialchars($_POST['date_from']) . '" size="19">';
  293. $str3 = '<input type="text" name="date_to" value="'
  294. . htmlspecialchars($_POST['date_to']) . '" size="19">';
  295. $str4 = '<input type="text" name="users" value="'
  296. . htmlspecialchars($_POST['users']) . '">';
  297. $str5 = '<input type="hidden" name="list_report" value="1">'
  298. . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">';
  299. return [
  300. $str1,
  301. $str2,
  302. $str3,
  303. $str4,
  304. $str5,
  305. ];
  306. }
  307. /**
  308. * Generate HTML for export form
  309. *
  310. * @param array $data data
  311. * @param array $url_params url params
  312. * @param bool $selection_schema selection schema
  313. * @param bool $selection_data selection data
  314. * @param bool $selection_both selection both
  315. * @param int $filter_ts_to filter time stamp from
  316. * @param int $filter_ts_from filter time stamp tp
  317. * @param array $filter_users filter users
  318. * @param string $str1 HTML for logtype select
  319. * @param string $str2 HTML for "from date"
  320. * @param string $str3 HTML for "to date"
  321. * @param string $str4 HTML for user
  322. * @param string $str5 HTML for "list report"
  323. * @param string $drop_image_or_text HTML for image or text
  324. *
  325. * @return string HTML for form
  326. */
  327. public function getHtmlForTrackingReportExportForm1(
  328. array $data,
  329. array $url_params,
  330. $selection_schema,
  331. $selection_data,
  332. $selection_both,
  333. $filter_ts_to,
  334. $filter_ts_from,
  335. array $filter_users,
  336. $str1,
  337. $str2,
  338. $str3,
  339. $str4,
  340. $str5,
  341. $drop_image_or_text
  342. ) {
  343. $ddlog_count = 0;
  344. $html = '<form method="post" action="' . Url::getFromRoute('/table/tracking') . '">';
  345. $html .= Url::getHiddenInputs($url_params + [
  346. 'report' => 'true',
  347. 'version' => $_POST['version'],
  348. ]);
  349. $html .= sprintf(
  350. __('Show %1$s with dates from %2$s to %3$s by user %4$s %5$s'),
  351. $str1,
  352. $str2,
  353. $str3,
  354. $str4,
  355. $str5
  356. );
  357. if ($selection_schema || $selection_both && count($data['ddlog']) > 0) {
  358. [$temp, $ddlog_count] = $this->getHtmlForDataDefinitionStatements(
  359. $data,
  360. $filter_users,
  361. $filter_ts_from,
  362. $filter_ts_to,
  363. $url_params,
  364. $drop_image_or_text
  365. );
  366. $html .= $temp;
  367. unset($temp);
  368. }
  369. /*
  370. * Secondly, list tracked data manipulation statements
  371. */
  372. if (($selection_data || $selection_both) && count($data['dmlog']) > 0) {
  373. $html .= $this->getHtmlForDataManipulationStatements(
  374. $data,
  375. $filter_users,
  376. $filter_ts_from,
  377. $filter_ts_to,
  378. $url_params,
  379. $ddlog_count,
  380. $drop_image_or_text
  381. );
  382. }
  383. $html .= '</form>';
  384. return $html;
  385. }
  386. /**
  387. * Generate HTML for export form
  388. *
  389. * @param array $url_params Parameters
  390. * @param string $str1 HTML for logtype select
  391. * @param string $str2 HTML for "from date"
  392. * @param string $str3 HTML for "to date"
  393. * @param string $str4 HTML for user
  394. * @param string $str5 HTML for "list report"
  395. *
  396. * @return string HTML for form
  397. */
  398. public function getHtmlForTrackingReportExportForm2(
  399. array $url_params,
  400. $str1,
  401. $str2,
  402. $str3,
  403. $str4,
  404. $str5
  405. ) {
  406. $html = '<form method="post" action="' . Url::getFromRoute('/table/tracking') . '">';
  407. $html .= Url::getHiddenInputs($url_params + [
  408. 'report' => 'true',
  409. 'version' => $_POST['version'],
  410. ]);
  411. $html .= sprintf(
  412. __('Show %1$s with dates from %2$s to %3$s by user %4$s %5$s'),
  413. $str1,
  414. $str2,
  415. $str3,
  416. $str4,
  417. $str5
  418. );
  419. $html .= '</form>';
  420. $html .= '<form class="disableAjax" method="post" action="' . Url::getFromRoute('/table/tracking') . '">';
  421. $html .= Url::getHiddenInputs($url_params + [
  422. 'report' => 'true',
  423. 'version' => $_POST['version'],
  424. 'logtype' => $_POST['logtype'],
  425. 'date_from' => $_POST['date_from'],
  426. 'date_to' => $_POST['date_to'],
  427. 'users' => $_POST['users'],
  428. 'report_export' => 'true',
  429. ]);
  430. $str_export1 = '<select name="export_type">'
  431. . '<option value="sqldumpfile">' . __('SQL dump (file download)')
  432. . '</option>'
  433. . '<option value="sqldump">' . __('SQL dump') . '</option>'
  434. . '<option value="execution" onclick="alert(\''
  435. . Sanitize::escapeJsString(
  436. __('This option will replace your table and contained data.')
  437. )
  438. . '\')">' . __('SQL execution') . '</option></select>';
  439. $str_export2 = '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">';
  440. $html .= '<br>' . sprintf(__('Export as %s'), $str_export1)
  441. . $str_export2 . '<br>';
  442. $html .= '</form>';
  443. return $html;
  444. }
  445. /**
  446. * Function to get html for data manipulation statements
  447. *
  448. * @param array $data data
  449. * @param array $filter_users filter users
  450. * @param int $filter_ts_from filter time staml from
  451. * @param int $filter_ts_to filter time stamp to
  452. * @param array $url_params url parameters
  453. * @param int $ddlog_count data definition log count
  454. * @param string $drop_image_or_text drop image or text
  455. *
  456. * @return string
  457. */
  458. public function getHtmlForDataManipulationStatements(
  459. array $data,
  460. array $filter_users,
  461. $filter_ts_from,
  462. $filter_ts_to,
  463. array $url_params,
  464. $ddlog_count,
  465. $drop_image_or_text
  466. ) {
  467. // no need for the second returned parameter
  468. [$html] = $this->getHtmlForDataStatements(
  469. $data,
  470. $filter_users,
  471. $filter_ts_from,
  472. $filter_ts_to,
  473. $url_params,
  474. $drop_image_or_text,
  475. 'dmlog',
  476. __('Data manipulation statement'),
  477. $ddlog_count,
  478. 'dml_versions'
  479. );
  480. return $html;
  481. }
  482. /**
  483. * Function to get html for data definition statements in schema snapshot
  484. *
  485. * @param array $data data
  486. * @param array $filter_users filter users
  487. * @param int $filter_ts_from filter time stamp from
  488. * @param int $filter_ts_to filter time stamp to
  489. * @param array $url_params url parameters
  490. * @param string $drop_image_or_text drop image or text
  491. *
  492. * @return array
  493. */
  494. public function getHtmlForDataDefinitionStatements(
  495. array $data,
  496. array $filter_users,
  497. $filter_ts_from,
  498. $filter_ts_to,
  499. array $url_params,
  500. $drop_image_or_text
  501. ) {
  502. [$html, $line_number] = $this->getHtmlForDataStatements(
  503. $data,
  504. $filter_users,
  505. $filter_ts_from,
  506. $filter_ts_to,
  507. $url_params,
  508. $drop_image_or_text,
  509. 'ddlog',
  510. __('Data definition statement'),
  511. 1,
  512. 'ddl_versions'
  513. );
  514. return [
  515. $html,
  516. $line_number,
  517. ];
  518. }
  519. /**
  520. * Function to get html for data statements in schema snapshot
  521. *
  522. * @param array $data data
  523. * @param array $filterUsers filter users
  524. * @param int $filterTsFrom filter time stamp from
  525. * @param int $filterTsTo filter time stamp to
  526. * @param array $urlParams url parameters
  527. * @param string $dropImageOrText drop image or text
  528. * @param string $whichLog dmlog|ddlog
  529. * @param string $headerMessage message for this section
  530. * @param int $lineNumber line number
  531. * @param string $tableId id for the table element
  532. *
  533. * @return array [$html, $lineNumber]
  534. */
  535. private function getHtmlForDataStatements(
  536. array $data,
  537. array $filterUsers,
  538. $filterTsFrom,
  539. $filterTsTo,
  540. array $urlParams,
  541. $dropImageOrText,
  542. $whichLog,
  543. $headerMessage,
  544. $lineNumber,
  545. $tableId
  546. ) {
  547. $offset = $lineNumber;
  548. $entries = [];
  549. foreach ($data[$whichLog] as $entry) {
  550. $timestamp = strtotime($entry['date']);
  551. if ($timestamp >= $filterTsFrom
  552. && $timestamp <= $filterTsTo
  553. && (in_array('*', $filterUsers)
  554. || in_array($entry['username'], $filterUsers))
  555. ) {
  556. $entry['formated_statement'] = Generator::formatSql($entry['statement'], true);
  557. $deleteParam = 'delete_' . $whichLog;
  558. $entry['url_params'] = Url::getCommon($urlParams + [
  559. 'report' => 'true',
  560. 'version' => $_POST['version'],
  561. $deleteParam => $lineNumber - $offset,
  562. ], '');
  563. $entry['line_number'] = $lineNumber;
  564. $entries[] = $entry;
  565. }
  566. $lineNumber++;
  567. }
  568. $html = $this->template->render('table/tracking/report_table', [
  569. 'table_id' => $tableId,
  570. 'header_message' => $headerMessage,
  571. 'entries' => $entries,
  572. 'drop_image_or_text' => $dropImageOrText,
  573. ]);
  574. return [
  575. $html,
  576. $lineNumber,
  577. ];
  578. }
  579. /**
  580. * Function to get html for schema snapshot
  581. *
  582. * @param array $params url parameters
  583. */
  584. public function getHtmlForSchemaSnapshot(array $params): string
  585. {
  586. $html = '<h3>' . __('Structure snapshot')
  587. . ' [<a href="' . Url::getFromRoute('/table/tracking', $params) . '">' . __('Close')
  588. . '</a>]</h3>';
  589. $data = Tracker::getTrackedData(
  590. $_POST['db'],
  591. $_POST['table'],
  592. $_POST['version']
  593. );
  594. // Get first DROP TABLE/VIEW and CREATE TABLE/VIEW statements
  595. $drop_create_statements = $data['ddlog'][0]['statement'];
  596. if (mb_strstr($data['ddlog'][0]['statement'], 'DROP TABLE')
  597. || mb_strstr($data['ddlog'][0]['statement'], 'DROP VIEW')
  598. ) {
  599. $drop_create_statements .= $data['ddlog'][1]['statement'];
  600. }
  601. // Print SQL code
  602. $html .= Generator::getMessage(
  603. sprintf(
  604. __('Version %s snapshot (SQL code)'),
  605. htmlspecialchars($_POST['version'])
  606. ),
  607. $drop_create_statements
  608. );
  609. // Unserialize snapshot
  610. $temp = Core::safeUnserialize($data['schema_snapshot']);
  611. if ($temp === null) {
  612. $temp = [
  613. 'COLUMNS' => [],
  614. 'INDEXES' => [],
  615. ];
  616. }
  617. $columns = $temp['COLUMNS'];
  618. $indexes = $temp['INDEXES'];
  619. $html .= $this->getHtmlForColumns($columns);
  620. if (count($indexes) > 0) {
  621. $html .= $this->getHtmlForIndexes($indexes);
  622. }
  623. $html .= '<br><hr><br>';
  624. return $html;
  625. }
  626. /**
  627. * Function to get html for displaying columns in the schema snapshot
  628. *
  629. * @param array $columns columns
  630. *
  631. * @return string
  632. */
  633. public function getHtmlForColumns(array $columns)
  634. {
  635. return $this->template->render('table/tracking/structure_snapshot_columns', ['columns' => $columns]);
  636. }
  637. /**
  638. * Function to get html for the indexes in schema snapshot
  639. *
  640. * @param array $indexes indexes
  641. *
  642. * @return string
  643. */
  644. public function getHtmlForIndexes(array $indexes)
  645. {
  646. return $this->template->render('table/tracking/structure_snapshot_indexes', ['indexes' => $indexes]);
  647. }
  648. /**
  649. * Function to handle the tracking report
  650. *
  651. * @param array $data tracked data
  652. *
  653. * @return string HTML for the message
  654. */
  655. public function deleteTrackingReportRows(array &$data)
  656. {
  657. $html = '';
  658. if (isset($_POST['delete_ddlog'])) {
  659. // Delete ddlog row data
  660. $html .= $this->deleteFromTrackingReportLog(
  661. $data,
  662. 'ddlog',
  663. 'DDL',
  664. __('Tracking data definition successfully deleted')
  665. );
  666. }
  667. if (isset($_POST['delete_dmlog'])) {
  668. // Delete dmlog row data
  669. $html .= $this->deleteFromTrackingReportLog(
  670. $data,
  671. 'dmlog',
  672. 'DML',
  673. __('Tracking data manipulation successfully deleted')
  674. );
  675. }
  676. return $html;
  677. }
  678. /**
  679. * Function to delete from a tracking report log
  680. *
  681. * @param array $data tracked data
  682. * @param string $which_log ddlog|dmlog
  683. * @param string $type DDL|DML
  684. * @param string $message success message
  685. *
  686. * @return string HTML for the message
  687. */
  688. public function deleteFromTrackingReportLog(array &$data, $which_log, $type, $message)
  689. {
  690. $html = '';
  691. $delete_id = $_POST['delete_' . $which_log];
  692. // Only in case of valid id
  693. if ($delete_id == (int) $delete_id) {
  694. unset($data[$which_log][$delete_id]);
  695. $successfullyDeleted = Tracker::changeTrackingData(
  696. $GLOBALS['db'],
  697. $GLOBALS['table'],
  698. $_POST['version'],
  699. $type,
  700. $data[$which_log]
  701. );
  702. if ($successfullyDeleted) {
  703. $msg = Message::success($message);
  704. } else {
  705. $msg = Message::rawError(__('Query error'));
  706. }
  707. $html .= $msg->getDisplay();
  708. }
  709. return $html;
  710. }
  711. /**
  712. * Function to export as sql dump
  713. *
  714. * @param array $entries entries
  715. *
  716. * @return string HTML SQL query form
  717. */
  718. public function exportAsSqlDump(array $entries)
  719. {
  720. $html = '';
  721. $new_query = '# '
  722. . __(
  723. 'You can execute the dump by creating and using a temporary database. '
  724. . 'Please ensure that you have the privileges to do so.'
  725. )
  726. . "\n"
  727. . '# ' . __('Comment out these two lines if you do not need them.') . "\n"
  728. . "\n"
  729. . "CREATE database IF NOT EXISTS pma_temp_db; \n"
  730. . "USE pma_temp_db; \n"
  731. . "\n";
  732. foreach ($entries as $entry) {
  733. $new_query .= $entry['statement'];
  734. }
  735. $msg = Message::success(
  736. __('SQL statements exported. Please copy the dump or execute it.')
  737. );
  738. $html .= $msg->getDisplay();
  739. $db_temp = $GLOBALS['db'];
  740. $table_temp = $GLOBALS['table'];
  741. $GLOBALS['db'] = $GLOBALS['table'] = '';
  742. $html .= $this->sqlQueryForm->getHtml($new_query, 'sql');
  743. $GLOBALS['db'] = $db_temp;
  744. $GLOBALS['table'] = $table_temp;
  745. return $html;
  746. }
  747. /**
  748. * Function to export as sql execution
  749. *
  750. * @param array $entries entries
  751. *
  752. * @return array
  753. */
  754. public function exportAsSqlExecution(array $entries)
  755. {
  756. global $dbi;
  757. $sql_result = [];
  758. foreach ($entries as $entry) {
  759. $sql_result = $dbi->query("/*NOTRACK*/\n" . $entry['statement']);
  760. }
  761. return $sql_result;
  762. }
  763. /**
  764. * Function to export as entries
  765. *
  766. * @param array $entries entries
  767. *
  768. * @return void
  769. */
  770. public function exportAsFileDownload(array $entries)
  771. {
  772. ini_set('url_rewriter.tags', '');
  773. // Replace all multiple whitespaces by a single space
  774. $table = htmlspecialchars(preg_replace('/\s+/', ' ', $_POST['table']));
  775. $dump = '# ' . sprintf(
  776. __('Tracking report for table `%s`'),
  777. $table
  778. )
  779. . "\n" . '# ' . date('Y-m-d H:i:s') . "\n";
  780. foreach ($entries as $entry) {
  781. $dump .= $entry['statement'];
  782. }
  783. $filename = 'log_' . $table . '.sql';
  784. Response::getInstance()->disable();
  785. Core::downloadHeader(
  786. $filename,
  787. 'text/x-sql',
  788. strlen($dump)
  789. );
  790. echo $dump;
  791. exit;
  792. }
  793. /**
  794. * Function to activate or deactivate tracking
  795. *
  796. * @param string $action activate|deactivate
  797. *
  798. * @return string HTML for the success message
  799. */
  800. public function changeTracking($action)
  801. {
  802. $html = '';
  803. if ($action === 'activate') {
  804. $method = 'activateTracking';
  805. $message = __('Tracking for %1$s was activated at version %2$s.');
  806. } else {
  807. $method = 'deactivateTracking';
  808. $message = __('Tracking for %1$s was deactivated at version %2$s.');
  809. }
  810. $status = Tracker::$method(
  811. $GLOBALS['db'],
  812. $GLOBALS['table'],
  813. $_POST['version']
  814. );
  815. if ($status) {
  816. $msg = Message::success(
  817. sprintf(
  818. $message,
  819. htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table']),
  820. htmlspecialchars($_POST['version'])
  821. )
  822. );
  823. $html .= $msg->getDisplay();
  824. }
  825. return $html;
  826. }
  827. /**
  828. * Function to get tracking set
  829. *
  830. * @return string
  831. */
  832. public function getTrackingSet()
  833. {
  834. $tracking_set = '';
  835. // a key is absent from the request if it has been removed from
  836. // tracking_default_statements in the config
  837. if (isset($_POST['alter_table']) && $_POST['alter_table'] == true) {
  838. $tracking_set .= 'ALTER TABLE,';
  839. }
  840. if (isset($_POST['rename_table']) && $_POST['rename_table'] == true) {
  841. $tracking_set .= 'RENAME TABLE,';
  842. }
  843. if (isset($_POST['create_table']) && $_POST['create_table'] == true) {
  844. $tracking_set .= 'CREATE TABLE,';
  845. }
  846. if (isset($_POST['drop_table']) && $_POST['drop_table'] == true) {
  847. $tracking_set .= 'DROP TABLE,';
  848. }
  849. if (isset($_POST['alter_view']) && $_POST['alter_view'] == true) {
  850. $tracking_set .= 'ALTER VIEW,';
  851. }
  852. if (isset($_POST['create_view']) && $_POST['create_view'] == true) {
  853. $tracking_set .= 'CREATE VIEW,';
  854. }
  855. if (isset($_POST['drop_view']) && $_POST['drop_view'] == true) {
  856. $tracking_set .= 'DROP VIEW,';
  857. }
  858. if (isset($_POST['create_index']) && $_POST['create_index'] == true) {
  859. $tracking_set .= 'CREATE INDEX,';
  860. }
  861. if (isset($_POST['drop_index']) && $_POST['drop_index'] == true) {
  862. $tracking_set .= 'DROP INDEX,';
  863. }
  864. if (isset($_POST['insert']) && $_POST['insert'] == true) {
  865. $tracking_set .= 'INSERT,';
  866. }
  867. if (isset($_POST['update']) && $_POST['update'] == true) {
  868. $tracking_set .= 'UPDATE,';
  869. }
  870. if (isset($_POST['delete']) && $_POST['delete'] == true) {
  871. $tracking_set .= 'DELETE,';
  872. }
  873. if (isset($_POST['truncate']) && $_POST['truncate'] == true) {
  874. $tracking_set .= 'TRUNCATE,';
  875. }
  876. $tracking_set = rtrim($tracking_set, ',');
  877. return $tracking_set;
  878. }
  879. /**
  880. * Deletes a tracking version
  881. *
  882. * @param string $version tracking version
  883. *
  884. * @return string HTML of the success message
  885. */
  886. public function deleteTrackingVersion($version)
  887. {
  888. $html = '';
  889. $versionDeleted = Tracker::deleteTracking(
  890. $GLOBALS['db'],
  891. $GLOBALS['table'],
  892. $version
  893. );
  894. if ($versionDeleted) {
  895. $msg = Message::success(
  896. sprintf(
  897. __('Version %1$s of %2$s was deleted.'),
  898. htmlspecialchars($version),
  899. htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table'])
  900. )
  901. );
  902. $html .= $msg->getDisplay();
  903. }
  904. return $html;
  905. }
  906. /**
  907. * Function to create the tracking version
  908. *
  909. * @return string HTML of the success message
  910. */
  911. public function createTrackingVersion()
  912. {
  913. global $dbi;
  914. $html = '';
  915. $tracking_set = $this->getTrackingSet();
  916. $versionCreated = Tracker::createVersion(
  917. $GLOBALS['db'],
  918. $GLOBALS['table'],
  919. $_POST['version'],
  920. $tracking_set,
  921. $dbi->getTable($GLOBALS['db'], $GLOBALS['table'])->isView()
  922. );
  923. if ($versionCreated) {
  924. $msg = Message::success(
  925. sprintf(
  926. __('Version %1$s was created, tracking for %2$s is active.'),
  927. htmlspecialchars($_POST['version']),
  928. htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table'])
  929. )
  930. );
  931. $html .= $msg->getDisplay();
  932. }
  933. return $html;
  934. }
  935. /**
  936. * Create tracking version for multiple tables
  937. *
  938. * @param array $selected list of selected tables
  939. *
  940. * @return void
  941. */
  942. public function createTrackingForMultipleTables(array $selected)
  943. {
  944. global $dbi;
  945. $tracking_set = $this->getTrackingSet();
  946. foreach ($selected as $selected_table) {
  947. Tracker::createVersion(
  948. $GLOBALS['db'],
  949. $selected_table,
  950. $_POST['version'],
  951. $tracking_set,
  952. $dbi->getTable($GLOBALS['db'], $selected_table)->isView()
  953. );
  954. }
  955. }
  956. /**
  957. * Function to get the entries
  958. *
  959. * @param array $data data
  960. * @param int $filter_ts_from filter time stamp from
  961. * @param int $filter_ts_to filter time stamp to
  962. * @param array $filter_users filter users
  963. *
  964. * @return array
  965. */
  966. public function getEntries(array $data, $filter_ts_from, $filter_ts_to, array $filter_users)
  967. {
  968. $entries = [];
  969. // Filtering data definition statements
  970. if ($_POST['logtype'] === 'schema'
  971. || $_POST['logtype'] === 'schema_and_data'
  972. ) {
  973. $entries = array_merge(
  974. $entries,
  975. $this->filter(
  976. $data['ddlog'],
  977. $filter_ts_from,
  978. $filter_ts_to,
  979. $filter_users
  980. )
  981. );
  982. }
  983. // Filtering data manipulation statements
  984. if ($_POST['logtype'] === 'data'
  985. || $_POST['logtype'] === 'schema_and_data'
  986. ) {
  987. $entries = array_merge(
  988. $entries,
  989. $this->filter(
  990. $data['dmlog'],
  991. $filter_ts_from,
  992. $filter_ts_to,
  993. $filter_users
  994. )
  995. );
  996. }
  997. // Sort it
  998. $ids = $timestamps = $usernames = $statements = [];
  999. foreach ($entries as $key => $row) {
  1000. $ids[$key] = $row['id'];
  1001. $timestamps[$key] = $row['timestamp'];
  1002. $usernames[$key] = $row['username'];
  1003. $statements[$key] = $row['statement'];
  1004. }
  1005. array_multisort(
  1006. $timestamps,
  1007. SORT_ASC,
  1008. $ids,
  1009. SORT_ASC,
  1010. $usernames,
  1011. SORT_ASC,
  1012. $statements,
  1013. SORT_ASC,
  1014. $entries
  1015. );
  1016. return $entries;
  1017. }
  1018. /**
  1019. * Get HTML for tracked and untracked tables
  1020. *
  1021. * @param string $db current database
  1022. * @param array $urlParams url parameters
  1023. * @param string $themeImagePath path to theme's image folder
  1024. * @param string $textDir text direction
  1025. *
  1026. * @return string HTML
  1027. */
  1028. public function getHtmlForDbTrackingTables(
  1029. string $db,
  1030. array $urlParams,
  1031. string $themeImagePath,
  1032. string $textDir
  1033. ) {
  1034. global $dbi;
  1035. $relation = $this->relation;
  1036. $cfgRelation = $relation->getRelationsParam();
  1037. // Prepare statement to get HEAD version
  1038. $allTablesQuery = ' SELECT table_name, MAX(version) as version FROM ' .
  1039. Util::backquote($cfgRelation['db']) . '.' .
  1040. Util::backquote($cfgRelation['tracking']) .
  1041. ' WHERE db_name = \'' . $dbi->escapeString($db) .
  1042. '\' ' .
  1043. ' GROUP BY table_name' .
  1044. ' ORDER BY table_name ASC';
  1045. $allTablesResult = $relation->queryAsControlUser($allTablesQuery);
  1046. $untrackedTables = $this->getUntrackedTables($db);
  1047. // If a HEAD version exists
  1048. $versions = [];
  1049. $headVersionExists = is_object($allTablesResult)
  1050. && $dbi->numRows($allTablesResult) > 0;
  1051. if ($headVersionExists) {
  1052. while ($oneResult = $dbi->fetchArray($allTablesResult)) {
  1053. [$tableName, $versionNumber] = $oneResult;
  1054. $tableQuery = ' SELECT * FROM ' .
  1055. Util::backquote($cfgRelation['db']) . '.' .
  1056. Util::backquote($cfgRelation['tracking']) .
  1057. ' WHERE `db_name` = \''
  1058. . $dbi->escapeString($db)
  1059. . '\' AND `table_name` = \''
  1060. . $dbi->escapeString($tableName)
  1061. . '\' AND `version` = \'' . $versionNumber . '\'';
  1062. $tableResult = $relation->queryAsControlUser($tableQuery);
  1063. $versions[] = $dbi->fetchArray($tableResult);
  1064. }
  1065. }
  1066. return $this->template->render('database/tracking/tables', [
  1067. 'db' => $db,
  1068. 'head_version_exists' => $headVersionExists,
  1069. 'untracked_tables_exists' => count($untrackedTables) > 0,
  1070. 'versions' => $versions,
  1071. 'url_params' => $urlParams,
  1072. 'text_dir' => $textDir,
  1073. 'untracked_tables' => $untrackedTables,
  1074. 'theme_image_path' => $themeImagePath,
  1075. ]);
  1076. }
  1077. /**
  1078. * Helper function: Recursive function for getting table names from $table_list
  1079. *
  1080. * @param array $table_list Table list
  1081. * @param string $db Current database
  1082. * @param bool $testing Testing
  1083. *
  1084. * @return array
  1085. */
  1086. public function extractTableNames(array $table_list, $db, $testing = false)
  1087. {
  1088. $untracked_tables = [];
  1089. $sep = $GLOBALS['cfg']['NavigationTreeTableSeparator'];
  1090. foreach ($table_list as $key => $value) {
  1091. if (is_array($value) && array_key_exists('is' . $sep . 'group', $value)
  1092. && $value['is' . $sep . 'group']
  1093. ) {
  1094. // Recursion step
  1095. $untracked_tables = array_merge($this->extractTableNames($value, $db, $testing), $untracked_tables);
  1096. } else {
  1097. if (is_array($value) && ($testing || Tracker::getVersion($db, $value['Name']) == -1)) {
  1098. $untracked_tables[] = $value['Name'];
  1099. }
  1100. }
  1101. }
  1102. return $untracked_tables;
  1103. }
  1104. /**
  1105. * Get untracked tables
  1106. *
  1107. * @param string $db current database
  1108. *
  1109. * @return array
  1110. */
  1111. public function getUntrackedTables($db)
  1112. {
  1113. $table_list = Util::getTableList($db);
  1114. //Use helper function to get table list recursively.
  1115. return $this->extractTableNames($table_list, $db);
  1116. }
  1117. }