3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Controllers\Table\Structure
;
7 use PhpMyAdmin\Controllers\Table\AbstractController
;
8 use PhpMyAdmin\Controllers\Table\StructureController
;
10 use PhpMyAdmin\DatabaseInterface
;
11 use PhpMyAdmin\Html\Generator
;
13 use PhpMyAdmin\Message
;
14 use PhpMyAdmin\Relation
;
15 use PhpMyAdmin\ResponseRenderer
;
17 use PhpMyAdmin\Template
;
18 use PhpMyAdmin\Transformations
;
25 use function in_array
;
26 use function is_array
;
27 use function mb_strpos
;
31 final class SaveController
extends AbstractController
33 /** @var Table The table object */
39 /** @var Transformations */
40 private $transformations;
42 /** @var DatabaseInterface */
45 /** @var StructureController */
46 private $structureController;
49 * @param ResponseRenderer $response
50 * @param string $db Database name
51 * @param string $table Table name
52 * @param DatabaseInterface $dbi
54 public function __construct(
60 Transformations
$transformations,
62 StructureController
$structureController
64 parent
::__construct($response, $template, $db, $table);
65 $this->relation
= $relation;
66 $this->transformations
= $transformations;
68 $this->structureController
= $structureController;
70 $this->tableObj
= $this->dbi
->getTable($this->db
, $this->table
);
73 public function __invoke(): void
75 $regenerate = $this->updateColumns();
77 // continue to show the table's structure
78 unset($_POST['selected']);
81 ($this->structureController
)();
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', [
93 'table' => $this->table
,
96 $field_cnt = count($_POST['field_name'] ??
[]);
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)) {
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}", ''),
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
134 (string) $sorted_col,
135 Util
::backquote($_POST['field_orig'][$i])
138 // delete the whole remembered sort expression
139 $this->tableObj
->removeUiProp(Table
::PROP_SORTED_COLUMN
);
143 ! isset($_POST['field_adjust_privileges'][$i])
144 ||
empty($_POST['field_adjust_privileges'][$i])
145 ||
$_POST['field_orig'][$i] == $_POST['field_name'][$i]
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
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
)) {
168 $this->dbi
->getError(),
169 'USE ' . Util
::backquote($this->db
) . ';',
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';
183 // If there is a request for SQL previewing.
184 if (isset($_POST['preview_sql'])) {
185 Core
::previewSQL(count($changes) > 0 ?
$sql_query : '');
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
198 // While changing the Column Collation
199 // First change to BLOB
200 for ($i = 0; $i < $field_cnt; $i++
) {
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(
209 . ' CHANGE ' . Util
::backquote(
210 $_POST['field_orig'][$i]
212 . ' ' . Util
::backquote($_POST['field_orig'][$i])
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;
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(
239 if ($changed_privileges) {
240 $message = Message
::success(
242 'Table %1$s has been altered successfully. Privileges ' .
243 'have been adjusted.'
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')
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]) {
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
)
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')
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) {
314 $this->relation
->renameField(
318 $_POST['field_name'][$fieldindex]
325 isset($_POST['field_mimetype'])
326 && is_array($_POST['field_mimetype'])
327 && $GLOBALS['cfg']['BrowseMIME']
329 foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
331 ! isset($_POST['field_name'][$fieldindex])
332 ||
strlen($_POST['field_name'][$fieldindex]) <= 0
337 $this->transformations
->setMime(
340 $_POST['field_name'][$fieldindex],
342 $_POST['field_transformation'][$fieldindex],
343 $_POST['field_transformation_options'][$fieldindex],
344 $_POST['field_input_transformation'][$fieldindex],
345 $_POST['field_input_transformation_options'][$fieldindex]
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]) {
381 'field_default_value',
382 'field_default_type',
388 foreach ($fields as $field) {
389 if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) {
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
403 * @return bool boolean whether at least one column privileges
406 private function adjustColumnPrivileges(array $adjust_privileges)
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) {
420 'UPDATE %s SET Column_name = "%s"
422 AND Table_name = "%s"
423 AND Column_name = "%s";',
424 Util
::backquote('columns_priv'),
432 // i.e. if atleast one column privileges adjusted
437 // Finally FLUSH the new privileges
438 $this->dbi
->query('FLUSH PRIVILEGES;');