MoveColumnsController.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table\Structure;
  4. use PhpMyAdmin\Controllers\Table\AbstractController;
  5. use PhpMyAdmin\DatabaseInterface;
  6. use PhpMyAdmin\Message;
  7. use PhpMyAdmin\ResponseRenderer;
  8. use PhpMyAdmin\SqlParser\Components\CreateDefinition;
  9. use PhpMyAdmin\SqlParser\Parser;
  10. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  11. use PhpMyAdmin\Template;
  12. use PhpMyAdmin\Util;
  13. use function __;
  14. use function array_diff;
  15. use function array_keys;
  16. use function array_search;
  17. use function array_splice;
  18. use function assert;
  19. use function count;
  20. use function implode;
  21. use function is_array;
  22. final class MoveColumnsController extends AbstractController
  23. {
  24. /** @var DatabaseInterface */
  25. private $dbi;
  26. public function __construct(
  27. ResponseRenderer $response,
  28. Template $template,
  29. string $db,
  30. string $table,
  31. DatabaseInterface $dbi
  32. ) {
  33. parent::__construct($response, $template, $db, $table);
  34. $this->dbi = $dbi;
  35. }
  36. public function __invoke(): void
  37. {
  38. $moveColumns = $_POST['move_columns'] ?? null;
  39. $previewSql = $_REQUEST['preview_sql'] ?? null;
  40. if (! is_array($moveColumns) || ! $this->response->isAjax()) {
  41. $this->response->setRequestStatus(false);
  42. return;
  43. }
  44. $this->dbi->selectDb($this->db);
  45. $createTableSql = $this->dbi->getTable($this->db, $this->table)->showCreate();
  46. $sql_query = $this->generateAlterTableSql($createTableSql, $moveColumns);
  47. if ($sql_query === null) {
  48. $this->response->setRequestStatus(false);
  49. return;
  50. }
  51. if ($previewSql) {
  52. $this->response->addJSON(
  53. 'sql_data',
  54. $this->template->render('preview_sql', ['query_data' => $sql_query])
  55. );
  56. return;
  57. }
  58. $this->dbi->tryQuery($sql_query);
  59. $tmp_error = $this->dbi->getError();
  60. if ($tmp_error !== '') {
  61. $this->response->setRequestStatus(false);
  62. $this->response->addJSON('message', Message::error($tmp_error));
  63. return;
  64. }
  65. $message = Message::success(
  66. __('The columns have been moved successfully.')
  67. );
  68. $this->response->addJSON('message', $message);
  69. $this->response->addJSON('columns', $moveColumns);
  70. }
  71. /**
  72. * @param array<int,mixed> $moveColumns
  73. * @psalm-param list<mixed> $moveColumns
  74. */
  75. private function generateAlterTableSql(string $createTableSql, array $moveColumns): ?string
  76. {
  77. $parser = new Parser($createTableSql);
  78. /** @var CreateStatement $statement */
  79. $statement = $parser->statements[0];
  80. /** @var CreateDefinition[] $fields */
  81. $fields = $statement->fields;
  82. $columns = [];
  83. foreach ($fields as $field) {
  84. if ($field->name === null) {
  85. continue;
  86. }
  87. $columns[$field->name] = $field;
  88. }
  89. $columnNames = array_keys($columns);
  90. // Ensure the columns from client match the columns from the table
  91. if (
  92. count($columnNames) !== count($moveColumns) ||
  93. array_diff($columnNames, $moveColumns) !== []
  94. ) {
  95. return null;
  96. }
  97. $changes = [];
  98. // move columns from first to last
  99. /** @psalm-var list<string> $moveColumns */
  100. foreach ($moveColumns as $i => $columnName) {
  101. // is this column already correctly placed?
  102. if ($columnNames[$i] == $columnName) {
  103. continue;
  104. }
  105. $changes[] =
  106. 'CHANGE ' . Util::backquote($columnName) . ' ' . CreateDefinition::build($columns[$columnName]) .
  107. ($i === 0 ? ' FIRST' : ' AFTER ' . Util::backquote($columnNames[$i - 1]));
  108. // Move column to its new position
  109. /** @var int $j */
  110. $j = array_search($columnName, $columnNames, true);
  111. array_splice($columnNames, $j, 1);
  112. array_splice($columnNames, $i, 0, $columnName);
  113. }
  114. if ($changes === []) {
  115. return null;
  116. }
  117. assert($statement->name !== null, 'Alter table statement has no name');
  118. return 'ALTER TABLE ' . Util::backquote($statement->name->table) . "\n " . implode(",\n ", $changes);
  119. }
  120. }