OperationsController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Database;
  4. use PhpMyAdmin\Charsets;
  5. use PhpMyAdmin\CheckUserPrivileges;
  6. use PhpMyAdmin\DatabaseInterface;
  7. use PhpMyAdmin\Html\Generator;
  8. use PhpMyAdmin\Message;
  9. use PhpMyAdmin\Operations;
  10. use PhpMyAdmin\Plugins;
  11. use PhpMyAdmin\Plugins\Export\ExportSql;
  12. use PhpMyAdmin\Query\Utilities;
  13. use PhpMyAdmin\Relation;
  14. use PhpMyAdmin\RelationCleanup;
  15. use PhpMyAdmin\Response;
  16. use PhpMyAdmin\Template;
  17. use PhpMyAdmin\Url;
  18. use PhpMyAdmin\Util;
  19. use function count;
  20. use function mb_strtolower;
  21. use function strlen;
  22. /**
  23. * Handles miscellaneous database operations.
  24. */
  25. class OperationsController extends AbstractController
  26. {
  27. /** @var Operations */
  28. private $operations;
  29. /** @var CheckUserPrivileges */
  30. private $checkUserPrivileges;
  31. /** @var Relation */
  32. private $relation;
  33. /** @var RelationCleanup */
  34. private $relationCleanup;
  35. /** @var DatabaseInterface */
  36. private $dbi;
  37. /**
  38. * @param Response $response
  39. * @param string $db Database name
  40. * @param DatabaseInterface $dbi
  41. */
  42. public function __construct(
  43. $response,
  44. Template $template,
  45. $db,
  46. Operations $operations,
  47. CheckUserPrivileges $checkUserPrivileges,
  48. Relation $relation,
  49. RelationCleanup $relationCleanup,
  50. $dbi
  51. ) {
  52. parent::__construct($response, $template, $db);
  53. $this->operations = $operations;
  54. $this->checkUserPrivileges = $checkUserPrivileges;
  55. $this->relation = $relation;
  56. $this->relationCleanup = $relationCleanup;
  57. $this->dbi = $dbi;
  58. }
  59. public function index(): void
  60. {
  61. global $cfg, $db, $server, $sql_query, $move, $message, $tables_full, $err_url;
  62. global $export_sql_plugin, $views, $sqlConstratints, $local_query, $reload, $url_params, $tables;
  63. global $total_num_tables, $sub_part, $tooltip_truename;
  64. global $db_collation, $tooltip_aliasname, $pos, $is_information_schema, $single_table, $num_tables;
  65. $this->checkUserPrivileges->getPrivileges();
  66. $this->addScriptFiles(['database/operations.js']);
  67. $sql_query = '';
  68. /**
  69. * Rename/move or copy database
  70. */
  71. if (strlen($db) > 0
  72. && (! empty($_POST['db_rename']) || ! empty($_POST['db_copy']))
  73. ) {
  74. if (! empty($_POST['db_rename'])) {
  75. $move = true;
  76. } else {
  77. $move = false;
  78. }
  79. if (! isset($_POST['newname']) || strlen($_POST['newname']) === 0) {
  80. $message = Message::error(__('The database name is empty!'));
  81. } else {
  82. // lower_case_table_names=1 `DB` becomes `db`
  83. if ($this->dbi->getLowerCaseNames() === '1') {
  84. $_POST['newname'] = mb_strtolower(
  85. $_POST['newname']
  86. );
  87. }
  88. if ($_POST['newname'] === $_REQUEST['db']) {
  89. $message = Message::error(
  90. __('Cannot copy database to the same name. Change the name and try again.')
  91. );
  92. } else {
  93. $_error = false;
  94. if ($move || ! empty($_POST['create_database_before_copying'])) {
  95. $this->operations->createDbBeforeCopy();
  96. }
  97. // here I don't use DELIMITER because it's not part of the
  98. // language; I have to send each statement one by one
  99. // to avoid selecting alternatively the current and new db
  100. // we would need to modify the CREATE definitions to qualify
  101. // the db name
  102. $this->operations->runProcedureAndFunctionDefinitions($db);
  103. // go back to current db, just in case
  104. $this->dbi->selectDb($db);
  105. $tables_full = $this->dbi->getTablesFull($db);
  106. // remove all foreign key constraints, otherwise we can get errors
  107. /** @var ExportSql $export_sql_plugin */
  108. $export_sql_plugin = Plugins::getPlugin(
  109. 'export',
  110. 'sql',
  111. 'libraries/classes/Plugins/Export/',
  112. [
  113. 'single_table' => isset($single_table),
  114. 'export_type' => 'database',
  115. ]
  116. );
  117. // create stand-in tables for views
  118. $views = $this->operations->getViewsAndCreateSqlViewStandIn(
  119. $tables_full,
  120. $export_sql_plugin,
  121. $db
  122. );
  123. // copy tables
  124. $sqlConstratints = $this->operations->copyTables(
  125. $tables_full,
  126. $move,
  127. $db
  128. );
  129. // handle the views
  130. if (! $_error) {
  131. $this->operations->handleTheViews($views, $move, $db);
  132. }
  133. unset($views);
  134. // now that all tables exist, create all the accumulated constraints
  135. if (! $_error && count($sqlConstratints) > 0) {
  136. $this->operations->createAllAccumulatedConstraints($sqlConstratints);
  137. }
  138. unset($sqlConstratints);
  139. if ($this->dbi->getVersion() >= 50100) {
  140. // here DELIMITER is not used because it's not part of the
  141. // language; each statement is sent one by one
  142. $this->operations->runEventDefinitionsForDb($db);
  143. }
  144. // go back to current db, just in case
  145. $this->dbi->selectDb($db);
  146. // Duplicate the bookmarks for this db (done once for each db)
  147. $this->operations->duplicateBookmarks($_error, $db);
  148. if (! $_error && $move) {
  149. if (isset($_POST['adjust_privileges'])
  150. && ! empty($_POST['adjust_privileges'])
  151. ) {
  152. $this->operations->adjustPrivilegesMoveDb($db, $_POST['newname']);
  153. }
  154. /**
  155. * cleanup pmadb stuff for this db
  156. */
  157. $this->relationCleanup->database($db);
  158. // if someday the RENAME DATABASE reappears, do not DROP
  159. $local_query = 'DROP DATABASE '
  160. . Util::backquote($db) . ';';
  161. $sql_query .= "\n" . $local_query;
  162. $this->dbi->query($local_query);
  163. $message = Message::success(
  164. __('Database %1$s has been renamed to %2$s.')
  165. );
  166. $message->addParam($db);
  167. $message->addParam($_POST['newname']);
  168. } elseif (! $_error) {
  169. if (isset($_POST['adjust_privileges'])
  170. && ! empty($_POST['adjust_privileges'])
  171. ) {
  172. $this->operations->adjustPrivilegesCopyDb($db, $_POST['newname']);
  173. }
  174. $message = Message::success(
  175. __('Database %1$s has been copied to %2$s.')
  176. );
  177. $message->addParam($db);
  178. $message->addParam($_POST['newname']);
  179. } else {
  180. $message = Message::error();
  181. }
  182. $reload = true;
  183. /* Change database to be used */
  184. if (! $_error && $move) {
  185. $db = $_POST['newname'];
  186. } elseif (! $_error) {
  187. if (isset($_POST['switch_to_new'])
  188. && $_POST['switch_to_new'] === 'true'
  189. ) {
  190. $_SESSION['pma_switch_to_new'] = true;
  191. $db = $_POST['newname'];
  192. } else {
  193. $_SESSION['pma_switch_to_new'] = false;
  194. }
  195. }
  196. }
  197. }
  198. /**
  199. * Database has been successfully renamed/moved. If in an Ajax request,
  200. * generate the output with {@link Response} and exit
  201. */
  202. if ($this->response->isAjax()) {
  203. $this->response->setRequestStatus($message->isSuccess());
  204. $this->response->addJSON('message', $message);
  205. $this->response->addJSON('newname', $_POST['newname']);
  206. $this->response->addJSON(
  207. 'sql_query',
  208. Generator::getMessage('', $sql_query)
  209. );
  210. $this->response->addJSON('db', $db);
  211. return;
  212. }
  213. }
  214. /**
  215. * Settings for relations stuff
  216. */
  217. $cfgRelation = $this->relation->getRelationsParam();
  218. /**
  219. * Check if comments were updated
  220. * (must be done before displaying the menu tabs)
  221. */
  222. if (isset($_POST['comment'])) {
  223. $this->relation->setDbComment($db, $_POST['comment']);
  224. }
  225. Util::checkParameters(['db']);
  226. $err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
  227. $err_url .= Url::getCommon(['db' => $db], '&');
  228. if (! $this->hasDatabase()) {
  229. return;
  230. }
  231. $url_params['goto'] = Url::getFromRoute('/database/operations');
  232. // Gets the database structure
  233. $sub_part = '_structure';
  234. [
  235. $tables,
  236. $num_tables,
  237. $total_num_tables,
  238. $sub_part,,
  239. $isSystemSchema,
  240. $tooltip_truename,
  241. $tooltip_aliasname,
  242. $pos,
  243. ] = Util::getDbInfo($db, $sub_part ?? '');
  244. $oldMessage = '';
  245. if (isset($message)) {
  246. $oldMessage = Generator::getMessage($message, $sql_query);
  247. unset($message);
  248. }
  249. $db_collation = $this->dbi->getDbCollation($db);
  250. $is_information_schema = Utilities::isSystemSchema($db);
  251. if ($is_information_schema) {
  252. return;
  253. }
  254. $databaseComment = '';
  255. if ($cfgRelation['commwork']) {
  256. $databaseComment = $this->relation->getDbComment($db);
  257. }
  258. $hasAdjustPrivileges = $GLOBALS['db_priv'] && $GLOBALS['table_priv']
  259. && $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] && $GLOBALS['is_reload_priv'];
  260. $isDropDatabaseAllowed = ($this->dbi->isSuperUser() || $cfg['AllowUserDropDatabase'])
  261. && ! $isSystemSchema && $db !== 'mysql';
  262. $switchToNew = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new'];
  263. $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  264. $collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  265. if (! $cfgRelation['allworks']
  266. && $cfg['PmaNoRelation_DisableWarning'] == false
  267. ) {
  268. $message = Message::notice(
  269. __(
  270. 'The phpMyAdmin configuration storage has been deactivated. ' .
  271. '%sFind out why%s.'
  272. )
  273. );
  274. $message->addParamHtml(
  275. '<a href="' . Url::getFromRoute('/check-relations')
  276. . '" data-post="' . Url::getCommon(['db' => $db]) . '">'
  277. );
  278. $message->addParamHtml('</a>');
  279. /* Show error if user has configured something, notice elsewhere */
  280. if (! empty($cfg['Servers'][$server]['pmadb'])) {
  281. $message->isError(true);
  282. }
  283. }
  284. $this->render('database/operations/index', [
  285. 'message' => $oldMessage,
  286. 'db' => $db,
  287. 'has_comment' => $cfgRelation['commwork'],
  288. 'db_comment' => $databaseComment,
  289. 'db_collation' => $db_collation,
  290. 'has_adjust_privileges' => $hasAdjustPrivileges,
  291. 'is_drop_database_allowed' => $isDropDatabaseAllowed,
  292. 'switch_to_new' => $switchToNew,
  293. 'charsets' => $charsets,
  294. 'collations' => $collations,
  295. ]);
  296. }
  297. public function collation(): void
  298. {
  299. global $db, $cfg, $err_url;
  300. if (! $this->response->isAjax()) {
  301. return;
  302. }
  303. if (empty($_POST['db_collation'])) {
  304. $this->response->setRequestStatus(false);
  305. $this->response->addJSON('message', Message::error(__('No collation provided.')));
  306. return;
  307. }
  308. Util::checkParameters(['db']);
  309. $err_url = Util::getScriptNameForOption($cfg['DefaultTabDatabase'], 'database');
  310. $err_url .= Url::getCommon(['db' => $db], '&');
  311. if (! $this->hasDatabase()) {
  312. return;
  313. }
  314. $sql_query = 'ALTER DATABASE ' . Util::backquote($db)
  315. . ' DEFAULT' . Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
  316. $this->dbi->query($sql_query);
  317. $message = Message::success();
  318. /**
  319. * Changes tables charset if requested by the user
  320. */
  321. if (isset($_POST['change_all_tables_collations']) &&
  322. $_POST['change_all_tables_collations'] === 'on'
  323. ) {
  324. [$tables] = Util::getDbInfo($db, null);
  325. foreach ($tables as $tableName => $data) {
  326. if ($this->dbi->getTable($db, $tableName)->isView()) {
  327. // Skip views, we can not change the collation of a view.
  328. // issue #15283
  329. continue;
  330. }
  331. $sql_query = 'ALTER TABLE '
  332. . Util::backquote($db)
  333. . '.'
  334. . Util::backquote($tableName)
  335. . ' DEFAULT '
  336. . Util::getCharsetQueryPart($_POST['db_collation'] ?? '');
  337. $this->dbi->query($sql_query);
  338. /**
  339. * Changes columns charset if requested by the user
  340. */
  341. if (! isset($_POST['change_all_tables_columns_collations']) ||
  342. $_POST['change_all_tables_columns_collations'] !== 'on'
  343. ) {
  344. continue;
  345. }
  346. $this->operations->changeAllColumnsCollation($db, $tableName, $_POST['db_collation']);
  347. }
  348. }
  349. $this->response->setRequestStatus($message->isSuccess());
  350. $this->response->addJSON('message', $message);
  351. }
  352. }