RecentFavoriteTable.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. /**
  3. * Recent and Favorite table list handling
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\ConfigStorage\Relation;
  8. use function __;
  9. use function array_key_exists;
  10. use function array_merge;
  11. use function array_pop;
  12. use function array_unique;
  13. use function array_unshift;
  14. use function count;
  15. use function is_string;
  16. use function json_decode;
  17. use function json_encode;
  18. use function max;
  19. use function md5;
  20. use function ucfirst;
  21. use const SORT_REGULAR;
  22. /**
  23. * Handles the recently used and favorite tables.
  24. *
  25. * @TODO Change the release version in table pma_recent
  26. * (#recent in documentation)
  27. */
  28. class RecentFavoriteTable
  29. {
  30. /** @var Template */
  31. public $template;
  32. /**
  33. * Reference to session variable containing recently used or favorite tables.
  34. *
  35. * @var array
  36. */
  37. private $tables;
  38. /**
  39. * Defines type of action, Favorite or Recent table.
  40. *
  41. * @var string
  42. */
  43. private $tableType;
  44. /**
  45. * RecentFavoriteTable instances.
  46. *
  47. * @var array<string,RecentFavoriteTable>
  48. */
  49. private static $instances = [];
  50. /** @var Relation */
  51. private $relation;
  52. /**
  53. * Creates a new instance of RecentFavoriteTable
  54. *
  55. * @param Template $template Template object
  56. * @param string $type the table type
  57. * @phpstan-param 'favorite'|'recent' $type
  58. */
  59. private function __construct(Template $template, string $type)
  60. {
  61. $this->template = $template;
  62. global $dbi;
  63. $this->relation = new Relation($dbi);
  64. $this->tableType = $type;
  65. $server_id = $GLOBALS['server'];
  66. // Code search hint: recentTables
  67. // Code search hint: favoriteTables
  68. if (! isset($_SESSION['tmpval'][$this->tableType . 'Tables'][$server_id])) {
  69. $_SESSION['tmpval'][$this->tableType . 'Tables'][$server_id] = $this->getPmaTable()
  70. ? $this->getFromDb()
  71. : [];
  72. }
  73. $this->tables =& $_SESSION['tmpval'][$this->tableType . 'Tables'][$server_id];
  74. }
  75. /**
  76. * Returns class instance.
  77. *
  78. * @param string $type the table type
  79. * @psalm-param 'favorite'|'recent' $type
  80. */
  81. public static function getInstance(string $type): RecentFavoriteTable
  82. {
  83. if (! array_key_exists($type, self::$instances)) {
  84. $template = new Template();
  85. self::$instances[$type] = new RecentFavoriteTable($template, $type);
  86. }
  87. return self::$instances[$type];
  88. }
  89. /**
  90. * Returns the recent/favorite tables array
  91. *
  92. * @return array
  93. */
  94. public function getTables()
  95. {
  96. return $this->tables;
  97. }
  98. /**
  99. * Returns recently used tables or favorite from phpMyAdmin database.
  100. *
  101. * @return array
  102. */
  103. public function getFromDb(): array
  104. {
  105. global $dbi;
  106. // Read from phpMyAdmin database, if recent tables is not in session
  107. $sql_query = ' SELECT `tables` FROM ' . $this->getPmaTable() .
  108. " WHERE `username` = '" . $dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'";
  109. $result = $dbi->tryQueryAsControlUser($sql_query);
  110. if ($result) {
  111. $value = $result->fetchValue();
  112. if (is_string($value)) {
  113. return json_decode($value, true);
  114. }
  115. }
  116. return [];
  117. }
  118. /**
  119. * Save recent/favorite tables into phpMyAdmin database.
  120. *
  121. * @return true|Message
  122. */
  123. public function saveToDb()
  124. {
  125. global $dbi;
  126. $username = $GLOBALS['cfg']['Server']['user'];
  127. $sql_query = ' REPLACE INTO ' . $this->getPmaTable() . ' (`username`, `tables`)' .
  128. " VALUES ('" . $dbi->escapeString($username) . "', '"
  129. . $dbi->escapeString(
  130. json_encode($this->tables)
  131. ) . "')";
  132. $success = $dbi->tryQuery($sql_query, DatabaseInterface::CONNECT_CONTROL);
  133. if (! $success) {
  134. $error_msg = '';
  135. switch ($this->tableType) {
  136. case 'recent':
  137. $error_msg = __('Could not save recent table!');
  138. break;
  139. case 'favorite':
  140. $error_msg = __('Could not save favorite table!');
  141. break;
  142. }
  143. $message = Message::error($error_msg);
  144. $message->addMessage(
  145. Message::rawError($dbi->getError(DatabaseInterface::CONNECT_CONTROL)),
  146. '<br><br>'
  147. );
  148. return $message;
  149. }
  150. return true;
  151. }
  152. /**
  153. * Trim recent.favorite table according to the
  154. * NumRecentTables/NumFavoriteTables configuration.
  155. */
  156. public function trim(): bool
  157. {
  158. $max = max(
  159. $GLOBALS['cfg']['Num' . ucfirst($this->tableType) . 'Tables'],
  160. 0
  161. );
  162. $trimmingOccurred = count($this->tables) > $max;
  163. while (count($this->tables) > $max) {
  164. array_pop($this->tables);
  165. }
  166. return $trimmingOccurred;
  167. }
  168. /**
  169. * Return HTML ul.
  170. */
  171. public function getHtmlList(): string
  172. {
  173. if (count($this->tables)) {
  174. if ($this->tableType === 'recent') {
  175. $tables = [];
  176. foreach ($this->tables as $table) {
  177. $tables[] = [
  178. 'db' => $table['db'],
  179. 'table' => $table['table'],
  180. ];
  181. }
  182. return $this->template->render('recent_favorite_table_recent', ['tables' => $tables]);
  183. }
  184. $tables = [];
  185. foreach ($this->tables as $table) {
  186. $removeParameters = [
  187. 'db' => $table['db'],
  188. 'ajax_request' => true,
  189. 'favorite_table' => $table['table'],
  190. 'remove_favorite' => true,
  191. ];
  192. $tableParameters = [
  193. 'db' => $table['db'],
  194. 'table' => $table['table'],
  195. 'md5' => md5($table['db'] . '.' . $table['table']),
  196. ];
  197. $tables[] = [
  198. 'remove_parameters' => $removeParameters,
  199. 'table_parameters' => $tableParameters,
  200. ];
  201. }
  202. return $this->template->render('recent_favorite_table_favorite', ['tables' => $tables]);
  203. }
  204. return $this->template->render('recent_favorite_table_no_tables', [
  205. 'is_recent' => $this->tableType === 'recent',
  206. ]);
  207. }
  208. public function getHtml(): string
  209. {
  210. $html = '<div class="drop_list">';
  211. if ($this->tableType === 'recent') {
  212. $html .= '<button title="' . __('Recent tables')
  213. . '" class="drop_button btn">'
  214. . __('Recent') . '</button><ul id="pma_recent_list">';
  215. } else {
  216. $html .= '<button title="' . __('Favorite tables')
  217. . '" class="drop_button btn">'
  218. . __('Favorites') . '</button><ul id="pma_favorite_list">';
  219. }
  220. $html .= $this->getHtmlList();
  221. $html .= '</ul></div>';
  222. return $html;
  223. }
  224. /**
  225. * Add recently used or favorite tables.
  226. *
  227. * @param string $db database name where the table is located
  228. * @param string $table table name
  229. *
  230. * @return true|Message True if success, Message if not
  231. */
  232. public function add($db, $table)
  233. {
  234. global $dbi;
  235. // If table does not exist, do not add._getPmaTable()
  236. if (! $dbi->getColumns($db, $table)) {
  237. return true;
  238. }
  239. $table_arr = [];
  240. $table_arr['db'] = $db;
  241. $table_arr['table'] = $table;
  242. // add only if this is new table
  243. if (! isset($this->tables[0]) || $this->tables[0] != $table_arr) {
  244. array_unshift($this->tables, $table_arr);
  245. $this->tables = array_merge(array_unique($this->tables, SORT_REGULAR));
  246. $this->trim();
  247. if ($this->getPmaTable()) {
  248. return $this->saveToDb();
  249. }
  250. }
  251. return true;
  252. }
  253. /**
  254. * Removes recent/favorite tables that don't exist.
  255. *
  256. * @param string $db database
  257. * @param string $table table
  258. *
  259. * @return bool|Message True if invalid and removed, False if not invalid,
  260. * Message if error while removing
  261. */
  262. public function removeIfInvalid($db, $table)
  263. {
  264. global $dbi;
  265. foreach ($this->tables as $tbl) {
  266. if ($tbl['db'] != $db || $tbl['table'] != $table) {
  267. continue;
  268. }
  269. // TODO Figure out a better way to find the existence of a table
  270. if (! $dbi->getColumns($tbl['db'], $tbl['table'])) {
  271. return $this->remove($tbl['db'], $tbl['table']);
  272. }
  273. }
  274. return false;
  275. }
  276. /**
  277. * Remove favorite tables.
  278. *
  279. * @param string $db database name where the table is located
  280. * @param string $table table name
  281. *
  282. * @return true|Message True if success, Message if not
  283. */
  284. public function remove($db, $table)
  285. {
  286. foreach ($this->tables as $key => $value) {
  287. if ($value['db'] != $db || $value['table'] != $table) {
  288. continue;
  289. }
  290. unset($this->tables[$key]);
  291. }
  292. if ($this->getPmaTable()) {
  293. return $this->saveToDb();
  294. }
  295. return true;
  296. }
  297. /**
  298. * Generate Html for sync Favorite tables anchor. (from localStorage to pmadb)
  299. */
  300. public function getHtmlSyncFavoriteTables(): string
  301. {
  302. $retval = '';
  303. $server_id = $GLOBALS['server'];
  304. if ($server_id == 0) {
  305. return '';
  306. }
  307. $relationParameters = $this->relation->getRelationParameters();
  308. // Not to show this once list is synchronized.
  309. if (
  310. $relationParameters->favoriteTablesFeature !== null
  311. && ! isset($_SESSION['tmpval']['favorites_synced'][$server_id])
  312. ) {
  313. $url = Url::getFromRoute('/database/structure/favorite-table', [
  314. 'ajax_request' => true,
  315. 'favorite_table' => true,
  316. 'sync_favorite_tables' => true,
  317. ]);
  318. $retval = '<a class="hide" id="sync_favorite_tables"';
  319. $retval .= ' href="' . $url . '"></a>';
  320. }
  321. return $retval;
  322. }
  323. /**
  324. * Generate Html to update recent tables.
  325. */
  326. public static function getHtmlUpdateRecentTables(): string
  327. {
  328. return '<a class="hide" id="update_recent_tables" href="'
  329. . Url::getFromRoute('/recent-table', [
  330. 'ajax_request' => true,
  331. 'recent_table' => true,
  332. ])
  333. . '"></a>';
  334. }
  335. /**
  336. * Return the name of the configuration storage table
  337. *
  338. * @return string|null pma table name
  339. */
  340. private function getPmaTable(): ?string
  341. {
  342. $relationParameters = $this->relation->getRelationParameters();
  343. if ($this->tableType === 'recent' && $relationParameters->recentlyUsedTablesFeature !== null) {
  344. return Util::backquote($relationParameters->recentlyUsedTablesFeature->database)
  345. . '.' . Util::backquote($relationParameters->recentlyUsedTablesFeature->recent);
  346. }
  347. if ($this->tableType === 'favorite' && $relationParameters->favoriteTablesFeature !== null) {
  348. return Util::backquote($relationParameters->favoriteTablesFeature->database)
  349. . '.' . Util::backquote($relationParameters->favoriteTablesFeature->favorite);
  350. }
  351. return null;
  352. }
  353. }