Extract actions from Table\StructureController
[phpmyadmin.git] / libraries / classes / Controllers / Table / Structure / SaveController.php
blob874f9a72570bb3df65b7030f7235275f1978b0b6
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Controllers\Table\Structure;
7 use PhpMyAdmin\Controllers\Table\AbstractController;
8 use PhpMyAdmin\Controllers\Table\StructureController;
9 use PhpMyAdmin\Core;
10 use PhpMyAdmin\DatabaseInterface;
11 use PhpMyAdmin\Html\Generator;
12 use PhpMyAdmin\Index;
13 use PhpMyAdmin\Message;
14 use PhpMyAdmin\Relation;
15 use PhpMyAdmin\ResponseRenderer;
16 use PhpMyAdmin\Table;
17 use PhpMyAdmin\Template;
18 use PhpMyAdmin\Transformations;
19 use PhpMyAdmin\Url;
20 use PhpMyAdmin\Util;
22 use function __;
23 use function count;
24 use function implode;
25 use function in_array;
26 use function is_array;
27 use function mb_strpos;
28 use function sprintf;
29 use function strlen;
31 final class SaveController extends AbstractController
33 /** @var Table The table object */
34 private $tableObj;
36 /** @var Relation */
37 private $relation;
39 /** @var Transformations */
40 private $transformations;
42 /** @var DatabaseInterface */
43 private $dbi;
45 /** @var StructureController */
46 private $structureController;
48 /**
49 * @param ResponseRenderer $response
50 * @param string $db Database name
51 * @param string $table Table name
52 * @param DatabaseInterface $dbi
54 public function __construct(
55 $response,
56 Template $template,
57 $db,
58 $table,
59 Relation $relation,
60 Transformations $transformations,
61 $dbi,
62 StructureController $structureController
63 ) {
64 parent::__construct($response, $template, $db, $table);
65 $this->relation = $relation;
66 $this->transformations = $transformations;
67 $this->dbi = $dbi;
68 $this->structureController = $structureController;
70 $this->tableObj = $this->dbi->getTable($this->db, $this->table);
73 public function __invoke(): void
75 $regenerate = $this->updateColumns();
76 if (! $regenerate) {
77 // continue to show the table's structure
78 unset($_POST['selected']);
81 ($this->structureController)();
84 /**
85 * Update the table's structure based on $_REQUEST
87 * @return bool true if error occurred
89 private function updateColumns()
91 $err_url = Url::getFromRoute('/table/structure', [
92 'db' => $this->db,
93 'table' => $this->table,
94 ]);
95 $regenerate = false;
96 $field_cnt = count($_POST['field_name'] ?? []);
97 $changes = [];
98 $adjust_privileges = [];
99 $columns_with_index = $this->dbi
100 ->getTable($this->db, $this->table)
101 ->getColumnsWithIndex(
102 Index::PRIMARY | Index::UNIQUE
104 for ($i = 0; $i < $field_cnt; $i++) {
105 if (! $this->columnNeedsAlterTable($i)) {
106 continue;
109 $changes[] = 'CHANGE ' . Table::generateAlter(
110 Util::getValueByKey($_POST, "field_orig.${i}", ''),
111 $_POST['field_name'][$i],
112 $_POST['field_type'][$i],
113 $_POST['field_length'][$i],
114 $_POST['field_attribute'][$i],
115 Util::getValueByKey($_POST, "field_collation.${i}", ''),
116 Util::getValueByKey($_POST, "field_null.${i}", 'NO'),
117 $_POST['field_default_type'][$i],
118 $_POST['field_default_value'][$i],
119 Util::getValueByKey($_POST, "field_extra.${i}", false),
120 Util::getValueByKey($_POST, "field_comments.${i}", ''),
121 Util::getValueByKey($_POST, "field_virtuality.${i}", ''),
122 Util::getValueByKey($_POST, "field_expression.${i}", ''),
123 Util::getValueByKey($_POST, "field_move_to.${i}", ''),
124 $columns_with_index
127 // find the remembered sort expression
128 $sorted_col = $this->tableObj->getUiProp(
129 Table::PROP_SORTED_COLUMN
131 // if the old column name is part of the remembered sort expression
132 if (
133 mb_strpos(
134 (string) $sorted_col,
135 Util::backquote($_POST['field_orig'][$i])
136 ) !== false
138 // delete the whole remembered sort expression
139 $this->tableObj->removeUiProp(Table::PROP_SORTED_COLUMN);
142 if (
143 ! isset($_POST['field_adjust_privileges'][$i])
144 || empty($_POST['field_adjust_privileges'][$i])
145 || $_POST['field_orig'][$i] == $_POST['field_name'][$i]
147 continue;
150 $adjust_privileges[$_POST['field_orig'][$i]] = $_POST['field_name'][$i];
153 if (count($changes) > 0 || isset($_POST['preview_sql'])) {
154 // Builds the primary keys statements and updates the table
155 $key_query = '';
157 * this is a little bit more complex
159 * @todo if someone selects A_I when altering a column we need to check:
160 * - no other column with A_I
161 * - the column has an index, if not create one
164 // To allow replication, we first select the db to use
165 // and then run queries on this db.
166 if (! $this->dbi->selectDb($this->db)) {
167 Generator::mysqlDie(
168 $this->dbi->getError(),
169 'USE ' . Util::backquote($this->db) . ';',
170 false,
171 $err_url
175 $sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' ';
176 $sql_query .= implode(', ', $changes) . $key_query;
177 if (isset($_POST['online_transaction'])) {
178 $sql_query .= ', ALGORITHM=INPLACE, LOCK=NONE';
181 $sql_query .= ';';
183 // If there is a request for SQL previewing.
184 if (isset($_POST['preview_sql'])) {
185 Core::previewSQL(count($changes) > 0 ? $sql_query : '');
187 exit;
190 $columns_with_index = $this->dbi
191 ->getTable($this->db, $this->table)
192 ->getColumnsWithIndex(
193 Index::PRIMARY | Index::UNIQUE | Index::INDEX
194 | Index::SPATIAL | Index::FULLTEXT
197 $changedToBlob = [];
198 // While changing the Column Collation
199 // First change to BLOB
200 for ($i = 0; $i < $field_cnt; $i++) {
201 if (
202 isset($_POST['field_collation'][$i], $_POST['field_collation_orig'][$i])
203 && $_POST['field_collation'][$i] !== $_POST['field_collation_orig'][$i]
204 && ! in_array($_POST['field_orig'][$i], $columns_with_index)
206 $secondary_query = 'ALTER TABLE ' . Util::backquote(
207 $this->table
209 . ' CHANGE ' . Util::backquote(
210 $_POST['field_orig'][$i]
212 . ' ' . Util::backquote($_POST['field_orig'][$i])
213 . ' BLOB';
215 if (isset($_POST['field_virtuality'][$i], $_POST['field_expression'][$i])) {
216 if ($_POST['field_virtuality'][$i]) {
217 $secondary_query .= ' AS (' . $_POST['field_expression'][$i] . ') '
218 . $_POST['field_virtuality'][$i];
222 $secondary_query .= ';';
224 $this->dbi->query($secondary_query);
225 $changedToBlob[$i] = true;
226 } else {
227 $changedToBlob[$i] = false;
231 // Then make the requested changes
232 $result = $this->dbi->tryQuery($sql_query);
234 if ($result !== false) {
235 $changed_privileges = $this->adjustColumnPrivileges(
236 $adjust_privileges
239 if ($changed_privileges) {
240 $message = Message::success(
242 'Table %1$s has been altered successfully. Privileges ' .
243 'have been adjusted.'
246 } else {
247 $message = Message::success(
248 __('Table %1$s has been altered successfully.')
252 $message->addParam($this->table);
254 $this->response->addHTML(
255 Generator::getMessage($message, $sql_query, 'success')
257 } else {
258 // An error happened while inserting/updating a table definition
260 // Save the Original Error
261 $orig_error = $this->dbi->getError();
262 $changes_revert = [];
264 // Change back to Original Collation and data type
265 for ($i = 0; $i < $field_cnt; $i++) {
266 if (! $changedToBlob[$i]) {
267 continue;
270 $changes_revert[] = 'CHANGE ' . Table::generateAlter(
271 Util::getValueByKey($_POST, "field_orig.${i}", ''),
272 $_POST['field_name'][$i],
273 $_POST['field_type_orig'][$i],
274 $_POST['field_length_orig'][$i],
275 $_POST['field_attribute_orig'][$i],
276 Util::getValueByKey($_POST, "field_collation_orig.${i}", ''),
277 Util::getValueByKey($_POST, "field_null_orig.${i}", 'NO'),
278 $_POST['field_default_type_orig'][$i],
279 $_POST['field_default_value_orig'][$i],
280 Util::getValueByKey($_POST, "field_extra_orig.${i}", false),
281 Util::getValueByKey($_POST, "field_comments_orig.${i}", ''),
282 Util::getValueByKey($_POST, "field_virtuality_orig.${i}", ''),
283 Util::getValueByKey($_POST, "field_expression_orig.${i}", ''),
284 Util::getValueByKey($_POST, "field_move_to_orig.${i}", '')
288 $revert_query = 'ALTER TABLE ' . Util::backquote($this->table)
289 . ' ';
290 $revert_query .= implode(', ', $changes_revert) . '';
291 $revert_query .= ';';
293 // Column reverted back to original
294 $this->dbi->query($revert_query);
296 $this->response->setRequestStatus(false);
297 $message = Message::rawError(
298 __('Query error') . ':<br>' . $orig_error
300 $this->response->addHTML(
301 Generator::getMessage($message, $sql_query, 'error')
303 $regenerate = true;
307 // update field names in relation
308 if (isset($_POST['field_orig']) && is_array($_POST['field_orig'])) {
309 foreach ($_POST['field_orig'] as $fieldindex => $fieldcontent) {
310 if ($_POST['field_name'][$fieldindex] == $fieldcontent) {
311 continue;
314 $this->relation->renameField(
315 $this->db,
316 $this->table,
317 $fieldcontent,
318 $_POST['field_name'][$fieldindex]
323 // update mime types
324 if (
325 isset($_POST['field_mimetype'])
326 && is_array($_POST['field_mimetype'])
327 && $GLOBALS['cfg']['BrowseMIME']
329 foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
330 if (
331 ! isset($_POST['field_name'][$fieldindex])
332 || strlen($_POST['field_name'][$fieldindex]) <= 0
334 continue;
337 $this->transformations->setMime(
338 $this->db,
339 $this->table,
340 $_POST['field_name'][$fieldindex],
341 $mimetype,
342 $_POST['field_transformation'][$fieldindex],
343 $_POST['field_transformation_options'][$fieldindex],
344 $_POST['field_input_transformation'][$fieldindex],
345 $_POST['field_input_transformation_options'][$fieldindex]
350 return $regenerate;
354 * Verifies if some elements of a column have changed
356 * @param int $i column index in the request
358 * @return bool true if we need to generate ALTER TABLE
360 private function columnNeedsAlterTable($i)
362 // these two fields are checkboxes so might not be part of the
363 // request; therefore we define them to avoid notices below
364 if (! isset($_POST['field_null'][$i])) {
365 $_POST['field_null'][$i] = 'NO';
368 if (! isset($_POST['field_extra'][$i])) {
369 $_POST['field_extra'][$i] = '';
372 // field_name does not follow the convention (corresponds to field_orig)
373 if ($_POST['field_name'][$i] != $_POST['field_orig'][$i]) {
374 return true;
377 $fields = [
378 'field_attribute',
379 'field_collation',
380 'field_comments',
381 'field_default_value',
382 'field_default_type',
383 'field_extra',
384 'field_length',
385 'field_null',
386 'field_type',
388 foreach ($fields as $field) {
389 if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) {
390 return true;
394 return ! empty($_POST['field_move_to'][$i]);
398 * Adjusts the Privileges for all the columns whose names have changed
400 * @param array $adjust_privileges assoc array of old col names mapped to new
401 * cols
403 * @return bool boolean whether at least one column privileges
404 * adjusted
406 private function adjustColumnPrivileges(array $adjust_privileges)
408 $changed = false;
410 if (
411 Util::getValueByKey($GLOBALS, 'col_priv', false)
412 && Util::getValueByKey($GLOBALS, 'is_reload_priv', false)
414 $this->dbi->selectDb('mysql');
416 // For Column specific privileges
417 foreach ($adjust_privileges as $oldCol => $newCol) {
418 $this->dbi->query(
419 sprintf(
420 'UPDATE %s SET Column_name = "%s"
421 WHERE Db = "%s"
422 AND Table_name = "%s"
423 AND Column_name = "%s";',
424 Util::backquote('columns_priv'),
425 $newCol,
426 $this->db,
427 $this->table,
428 $oldCol
432 // i.e. if atleast one column privileges adjusted
433 $changed = true;
436 if ($changed) {
437 // Finally FLUSH the new privileges
438 $this->dbi->query('FLUSH PRIVILEGES;');
442 return $changed;