DatabasesController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Server;
  4. use PhpMyAdmin\Charsets;
  5. use PhpMyAdmin\Charsets\Charset;
  6. use PhpMyAdmin\Charsets\Collation;
  7. use PhpMyAdmin\CheckUserPrivileges;
  8. use PhpMyAdmin\Controllers\AbstractController;
  9. use PhpMyAdmin\DatabaseInterface;
  10. use PhpMyAdmin\Html\Generator;
  11. use PhpMyAdmin\Message;
  12. use PhpMyAdmin\Query\Utilities;
  13. use PhpMyAdmin\RelationCleanup;
  14. use PhpMyAdmin\ReplicationInfo;
  15. use PhpMyAdmin\Response;
  16. use PhpMyAdmin\Template;
  17. use PhpMyAdmin\Transformations;
  18. use PhpMyAdmin\Url;
  19. use PhpMyAdmin\Util;
  20. use function array_key_exists;
  21. use function array_keys;
  22. use function array_search;
  23. use function count;
  24. use function explode;
  25. use function in_array;
  26. use function mb_strlen;
  27. use function mb_strtolower;
  28. use function strlen;
  29. use function strpos;
  30. /**
  31. * Handles viewing and creating and deleting databases
  32. */
  33. class DatabasesController extends AbstractController
  34. {
  35. /** @var array array of database details */
  36. private $databases = [];
  37. /** @var int number of databases */
  38. private $databaseCount = 0;
  39. /** @var string sort by column */
  40. private $sortBy;
  41. /** @var string sort order of databases */
  42. private $sortOrder;
  43. /** @var bool whether to show database statistics */
  44. private $hasStatistics;
  45. /** @var int position in list navigation */
  46. private $position;
  47. /** @var Transformations */
  48. private $transformations;
  49. /** @var RelationCleanup */
  50. private $relationCleanup;
  51. /** @var DatabaseInterface */
  52. private $dbi;
  53. /**
  54. * @param Response $response
  55. * @param DatabaseInterface $dbi
  56. */
  57. public function __construct(
  58. $response,
  59. Template $template,
  60. Transformations $transformations,
  61. RelationCleanup $relationCleanup,
  62. $dbi
  63. ) {
  64. parent::__construct($response, $template);
  65. $this->transformations = $transformations;
  66. $this->relationCleanup = $relationCleanup;
  67. $this->dbi = $dbi;
  68. $checkUserPrivileges = new CheckUserPrivileges($dbi);
  69. $checkUserPrivileges->getPrivileges();
  70. }
  71. public function index(): void
  72. {
  73. global $cfg, $server, $dblist, $is_create_db_priv;
  74. global $db_to_create, $text_dir, $PMA_Theme, $err_url;
  75. $params = [
  76. 'statistics' => $_REQUEST['statistics'] ?? null,
  77. 'pos' => $_REQUEST['pos'] ?? null,
  78. 'sort_by' => $_REQUEST['sort_by'] ?? null,
  79. 'sort_order' => $_REQUEST['sort_order'] ?? null,
  80. ];
  81. $this->addScriptFiles(['server/databases.js']);
  82. $err_url = Url::getFromRoute('/');
  83. if ($this->dbi->isSuperUser()) {
  84. $this->dbi->selectDb('mysql');
  85. }
  86. $replicationInfo = new ReplicationInfo($this->dbi);
  87. $replicationInfo->load($_POST['master_connection'] ?? null);
  88. $primaryInfo = $replicationInfo->getPrimaryInfo();
  89. $replicaInfo = $replicationInfo->getReplicaInfo();
  90. $this->setSortDetails($params['sort_by'], $params['sort_order']);
  91. $this->hasStatistics = ! empty($params['statistics']);
  92. $this->position = ! empty($params['pos']) ? (int) $params['pos'] : 0;
  93. /**
  94. * Gets the databases list
  95. */
  96. if ($server > 0) {
  97. $this->databases = $this->dbi->getDatabasesFull(
  98. null,
  99. $this->hasStatistics,
  100. DatabaseInterface::CONNECT_USER,
  101. $this->sortBy,
  102. $this->sortOrder,
  103. $this->position,
  104. true
  105. );
  106. $this->databaseCount = count($dblist->databases);
  107. }
  108. $urlParams = [
  109. 'statistics' => $this->hasStatistics,
  110. 'pos' => $this->position,
  111. 'sort_by' => $this->sortBy,
  112. 'sort_order' => $this->sortOrder,
  113. ];
  114. $databases = $this->getDatabases($primaryInfo, $replicaInfo);
  115. $charsetsList = [];
  116. if ($cfg['ShowCreateDb'] && $is_create_db_priv) {
  117. $charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']);
  118. $collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']);
  119. $serverCollation = $this->dbi->getServerCollation();
  120. /** @var Charset $charset */
  121. foreach ($charsets as $charset) {
  122. $collationsList = [];
  123. /** @var Collation $collation */
  124. foreach ($collations[$charset->getName()] as $collation) {
  125. $collationsList[] = [
  126. 'name' => $collation->getName(),
  127. 'description' => $collation->getDescription(),
  128. 'is_selected' => $serverCollation === $collation->getName(),
  129. ];
  130. }
  131. $charsetsList[] = [
  132. 'name' => $charset->getName(),
  133. 'description' => $charset->getDescription(),
  134. 'collations' => $collationsList,
  135. ];
  136. }
  137. }
  138. $headerStatistics = $this->getStatisticsColumns();
  139. $this->render('server/databases/index', [
  140. 'is_create_database_shown' => $cfg['ShowCreateDb'],
  141. 'has_create_database_privileges' => $is_create_db_priv,
  142. 'has_statistics' => $this->hasStatistics,
  143. 'database_to_create' => $db_to_create,
  144. 'databases' => $databases['databases'],
  145. 'total_statistics' => $databases['total_statistics'],
  146. 'header_statistics' => $headerStatistics,
  147. 'charsets' => $charsetsList,
  148. 'database_count' => $this->databaseCount,
  149. 'pos' => $this->position,
  150. 'url_params' => $urlParams,
  151. 'max_db_list' => $cfg['MaxDbList'],
  152. 'has_master_replication' => $primaryInfo['status'],
  153. 'has_slave_replication' => $replicaInfo['status'],
  154. 'is_drop_allowed' => $this->dbi->isSuperUser() || $cfg['AllowUserDropDatabase'],
  155. 'theme_image_path' => $PMA_Theme->getImgPath(),
  156. 'text_dir' => $text_dir,
  157. ]);
  158. }
  159. public function create(): void
  160. {
  161. global $cfg, $db;
  162. $params = [
  163. 'new_db' => $_POST['new_db'] ?? null,
  164. 'db_collation' => $_POST['db_collation'] ?? null,
  165. ];
  166. if (! isset($params['new_db']) || mb_strlen($params['new_db']) === 0 || ! $this->response->isAjax()) {
  167. $this->response->addJSON(['message' => Message::error()]);
  168. return;
  169. }
  170. // lower_case_table_names=1 `DB` becomes `db`
  171. if ($this->dbi->getLowerCaseNames() === '1') {
  172. $params['new_db'] = mb_strtolower(
  173. $params['new_db']
  174. );
  175. }
  176. /**
  177. * Builds and executes the db creation sql query
  178. */
  179. $sqlQuery = 'CREATE DATABASE ' . Util::backquote($params['new_db']);
  180. if (! empty($params['db_collation'])) {
  181. [$databaseCharset] = explode('_', $params['db_collation']);
  182. $charsets = Charsets::getCharsets(
  183. $this->dbi,
  184. $cfg['Server']['DisableIS']
  185. );
  186. $collations = Charsets::getCollations(
  187. $this->dbi,
  188. $cfg['Server']['DisableIS']
  189. );
  190. if (array_key_exists($databaseCharset, $charsets)
  191. && array_key_exists($params['db_collation'], $collations[$databaseCharset])
  192. ) {
  193. $sqlQuery .= ' DEFAULT'
  194. . Util::getCharsetQueryPart($params['db_collation']);
  195. }
  196. }
  197. $sqlQuery .= ';';
  198. $result = $this->dbi->tryQuery($sqlQuery);
  199. if (! $result) {
  200. // avoid displaying the not-created db name in header or navi panel
  201. $db = '';
  202. $message = Message::rawError((string) $this->dbi->getError());
  203. $json = ['message' => $message];
  204. $this->response->setRequestStatus(false);
  205. } else {
  206. $db = $params['new_db'];
  207. $message = Message::success(__('Database %1$s has been created.'));
  208. $message->addParam($params['new_db']);
  209. $scriptName = Util::getScriptNameForOption(
  210. $cfg['DefaultTabDatabase'],
  211. 'database'
  212. );
  213. $json = [
  214. 'message' => $message,
  215. 'sql_query' => Generator::getMessage('', $sqlQuery, 'success'),
  216. 'url' => $scriptName . Url::getCommon(
  217. ['db' => $params['new_db']],
  218. strpos($scriptName, '?') === false ? '?' : '&'
  219. ),
  220. ];
  221. }
  222. $this->response->addJSON($json);
  223. }
  224. /**
  225. * Handles dropping multiple databases
  226. */
  227. public function destroy(): void
  228. {
  229. global $selected, $err_url, $cfg, $dblist, $reload;
  230. $params = [
  231. 'drop_selected_dbs' => $_POST['drop_selected_dbs'] ?? null,
  232. 'selected_dbs' => $_POST['selected_dbs'] ?? null,
  233. ];
  234. /** @var Message|int $message */
  235. $message = -1;
  236. if (! isset($params['drop_selected_dbs'])
  237. || ! $this->response->isAjax()
  238. || (! $this->dbi->isSuperUser() && ! $cfg['AllowUserDropDatabase'])
  239. ) {
  240. $message = Message::error();
  241. $json = ['message' => $message];
  242. $this->response->setRequestStatus($message->isSuccess());
  243. $this->response->addJSON($json);
  244. return;
  245. }
  246. if (! isset($params['selected_dbs'])) {
  247. $message = Message::error(__('No databases selected.'));
  248. $json = ['message' => $message];
  249. $this->response->setRequestStatus($message->isSuccess());
  250. $this->response->addJSON($json);
  251. return;
  252. }
  253. $err_url = Url::getFromRoute('/server/databases');
  254. $selected = $_POST['selected_dbs'];
  255. $rebuildDatabaseList = false;
  256. $sqlQuery = '';
  257. $numberOfDatabases = count($selected);
  258. for ($i = 0; $i < $numberOfDatabases; $i++) {
  259. $this->relationCleanup->database($selected[$i]);
  260. $aQuery = 'DROP DATABASE ' . Util::backquote($selected[$i]);
  261. $reload = true;
  262. $rebuildDatabaseList = true;
  263. $sqlQuery .= $aQuery . ';' . "\n";
  264. $this->dbi->query($aQuery);
  265. $this->transformations->clear($selected[$i]);
  266. }
  267. if ($rebuildDatabaseList) {
  268. $dblist->databases->build();
  269. }
  270. if ($message === -1) { // no error message
  271. $message = Message::success(
  272. _ngettext(
  273. '%1$d database has been dropped successfully.',
  274. '%1$d databases have been dropped successfully.',
  275. $numberOfDatabases
  276. )
  277. );
  278. $message->addParam($numberOfDatabases);
  279. }
  280. $json = [];
  281. if ($message instanceof Message) {
  282. $json = ['message' => $message];
  283. $this->response->setRequestStatus($message->isSuccess());
  284. }
  285. $this->response->addJSON($json);
  286. }
  287. /**
  288. * Extracts parameters sort order and sort by
  289. *
  290. * @param string|null $sortBy sort by
  291. * @param string|null $sortOrder sort order
  292. */
  293. private function setSortDetails(?string $sortBy, ?string $sortOrder): void
  294. {
  295. if (empty($sortBy)) {
  296. $this->sortBy = 'SCHEMA_NAME';
  297. } else {
  298. $sortByAllowList = [
  299. 'SCHEMA_NAME',
  300. 'DEFAULT_COLLATION_NAME',
  301. 'SCHEMA_TABLES',
  302. 'SCHEMA_TABLE_ROWS',
  303. 'SCHEMA_DATA_LENGTH',
  304. 'SCHEMA_INDEX_LENGTH',
  305. 'SCHEMA_LENGTH',
  306. 'SCHEMA_DATA_FREE',
  307. ];
  308. $this->sortBy = 'SCHEMA_NAME';
  309. if (in_array($sortBy, $sortByAllowList)) {
  310. $this->sortBy = $sortBy;
  311. }
  312. }
  313. $this->sortOrder = 'asc';
  314. if (! isset($sortOrder)
  315. || mb_strtolower($sortOrder) !== 'desc'
  316. ) {
  317. return;
  318. }
  319. $this->sortOrder = 'desc';
  320. }
  321. /**
  322. * @param array $primaryInfo
  323. * @param array $replicaInfo
  324. *
  325. * @return array
  326. */
  327. private function getDatabases($primaryInfo, $replicaInfo): array
  328. {
  329. global $cfg;
  330. $databases = [];
  331. $totalStatistics = $this->getStatisticsColumns();
  332. foreach ($this->databases as $database) {
  333. $replication = [
  334. 'master' => ['status' => $primaryInfo['status']],
  335. 'slave' => ['status' => $replicaInfo['status']],
  336. ];
  337. if ($primaryInfo['status']) {
  338. $key = array_search($database['SCHEMA_NAME'], $primaryInfo['Ignore_DB']);
  339. $replication['master']['is_replicated'] = false;
  340. if (strlen((string) $key) === 0) {
  341. $key = array_search($database['SCHEMA_NAME'], $primaryInfo['Do_DB']);
  342. if (strlen((string) $key) > 0 || count($primaryInfo['Do_DB']) === 0) {
  343. $replication['master']['is_replicated'] = true;
  344. }
  345. }
  346. }
  347. if ($replicaInfo['status']) {
  348. $key = array_search($database['SCHEMA_NAME'], $replicaInfo['Ignore_DB']);
  349. $replication['slave']['is_replicated'] = false;
  350. if (strlen((string) $key) === 0) {
  351. $key = array_search($database['SCHEMA_NAME'], $replicaInfo['Do_DB']);
  352. if (strlen((string) $key) > 0 || count($replicaInfo['Do_DB']) === 0) {
  353. $replication['slave']['is_replicated'] = true;
  354. }
  355. }
  356. }
  357. $statistics = $this->getStatisticsColumns();
  358. if ($this->hasStatistics) {
  359. foreach (array_keys($statistics) as $key) {
  360. $statistics[$key]['raw'] = $database[$key] ?? null;
  361. $totalStatistics[$key]['raw'] += (int) $database[$key] ?? 0;
  362. }
  363. }
  364. $url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
  365. $url .= Url::getCommonRaw(
  366. ['db' => $database['SCHEMA_NAME']],
  367. strpos($url, '?') === false ? '?' : '&'
  368. );
  369. $databases[$database['SCHEMA_NAME']] = [
  370. 'name' => $database['SCHEMA_NAME'],
  371. 'collation' => [],
  372. 'statistics' => $statistics,
  373. 'replication' => $replication,
  374. 'is_system_schema' => Utilities::isSystemSchema(
  375. $database['SCHEMA_NAME'],
  376. true
  377. ),
  378. 'is_pmadb' => $database['SCHEMA_NAME'] === ($cfg['Server']['pmadb'] ?? ''),
  379. 'url' => $url,
  380. ];
  381. $collation = Charsets::findCollationByName(
  382. $this->dbi,
  383. $cfg['Server']['DisableIS'],
  384. $database['DEFAULT_COLLATION_NAME']
  385. );
  386. if ($collation === null) {
  387. continue;
  388. }
  389. $databases[$database['SCHEMA_NAME']]['collation'] = [
  390. 'name' => $collation->getName(),
  391. 'description' => $collation->getDescription(),
  392. ];
  393. }
  394. return [
  395. 'databases' => $databases,
  396. 'total_statistics' => $totalStatistics,
  397. ];
  398. }
  399. /**
  400. * Prepares the statistics columns
  401. *
  402. * @return array
  403. */
  404. private function getStatisticsColumns(): array
  405. {
  406. return [
  407. 'SCHEMA_TABLES' => [
  408. 'title' => __('Tables'),
  409. 'format' => 'number',
  410. 'raw' => 0,
  411. ],
  412. 'SCHEMA_TABLE_ROWS' => [
  413. 'title' => __('Rows'),
  414. 'format' => 'number',
  415. 'raw' => 0,
  416. ],
  417. 'SCHEMA_DATA_LENGTH' => [
  418. 'title' => __('Data'),
  419. 'format' => 'byte',
  420. 'raw' => 0,
  421. ],
  422. 'SCHEMA_INDEX_LENGTH' => [
  423. 'title' => __('Indexes'),
  424. 'format' => 'byte',
  425. 'raw' => 0,
  426. ],
  427. 'SCHEMA_LENGTH' => [
  428. 'title' => __('Total'),
  429. 'format' => 'byte',
  430. 'raw' => 0,
  431. ],
  432. 'SCHEMA_DATA_FREE' => [
  433. 'title' => __('Overhead'),
  434. 'format' => 'byte',
  435. 'raw' => 0,
  436. ],
  437. ];
  438. }
  439. }