3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Controllers\Operations
;
7 use PhpMyAdmin\Charsets
;
8 use PhpMyAdmin\CheckUserPrivileges
;
10 use PhpMyAdmin\ConfigStorage\Relation
;
11 use PhpMyAdmin\Controllers\AbstractController
;
12 use PhpMyAdmin\Current
;
13 use PhpMyAdmin\DatabaseInterface
;
14 use PhpMyAdmin\DbTableExists
;
15 use PhpMyAdmin\Html\Generator
;
16 use PhpMyAdmin\Http\ServerRequest
;
17 use PhpMyAdmin\Identifiers\DatabaseName
;
18 use PhpMyAdmin\Identifiers\TableName
;
20 use PhpMyAdmin\Message
;
21 use PhpMyAdmin\Operations
;
22 use PhpMyAdmin\Partitioning\Partition
;
23 use PhpMyAdmin\Query\Generator
as QueryGenerator
;
24 use PhpMyAdmin\Query\Utilities
;
25 use PhpMyAdmin\ResponseRenderer
;
26 use PhpMyAdmin\StorageEngine
;
27 use PhpMyAdmin\Template
;
29 use PhpMyAdmin\UserPrivileges
;
35 use function is_array
;
36 use function is_string
;
37 use function mb_strstr
;
38 use function mb_strtolower
;
39 use function mb_strtoupper
;
40 use function preg_replace
;
42 use function urldecode
;
44 class TableController
extends AbstractController
46 public function __construct(
47 ResponseRenderer
$response,
49 private Operations
$operations,
50 private CheckUserPrivileges
$checkUserPrivileges,
51 private Relation
$relation,
52 private DatabaseInterface
$dbi,
53 private readonly DbTableExists
$dbTableExists,
55 parent
::__construct($response, $template);
58 public function __invoke(ServerRequest
$request): void
60 $GLOBALS['urlParams'] ??
= null;
61 $GLOBALS['auto_increment'] ??
= null;
62 $GLOBALS['message_to_show'] ??
= null;
63 $GLOBALS['errorUrl'] ??
= null;
65 $this->checkUserPrivileges
->getPrivileges();
67 if ($this->dbi
->getLowerCaseNames() === 1) {
68 Current
::$table = mb_strtolower(Current
::$table);
71 $pmaTable = $this->dbi
->getTable(Current
::$database, Current
::$table);
73 $this->addScriptFiles(['table/operations.js']);
75 if (! $this->checkParameters(['db', 'table'])) {
79 $isSystemSchema = Utilities
::isSystemSchema(Current
::$database);
80 $GLOBALS['urlParams'] = ['db' => Current
::$database, 'table' => Current
::$table];
81 $config = Config
::getInstance();
82 $GLOBALS['errorUrl'] = Util
::getScriptNameForOption($config->settings
['DefaultTabTable'], 'table');
83 $GLOBALS['errorUrl'] .= Url
::getCommon($GLOBALS['urlParams'], '&');
85 $databaseName = DatabaseName
::tryFrom($request->getParam('db'));
86 if ($databaseName === null ||
! $this->dbTableExists
->selectDatabase($databaseName)) {
87 if ($request->isAjax()) {
88 $this->response
->setRequestStatus(false);
89 $this->response
->addJSON('message', Message
::error(__('No databases selected.')));
94 $this->redirect('/', ['reload' => true, 'message' => __('No databases selected.')]);
99 $tableName = TableName
::tryFrom($request->getParam('table'));
100 if ($tableName === null ||
! $this->dbTableExists
->hasTable($databaseName, $tableName)) {
101 if ($request->isAjax()) {
102 $this->response
->setRequestStatus(false);
103 $this->response
->addJSON('message', Message
::error(__('No table selected.')));
108 $this->redirect('/', ['reload' => true, 'message' => __('No table selected.')]);
113 $GLOBALS['urlParams']['goto'] = $GLOBALS['urlParams']['back'] = Url
::getFromRoute('/table/operations');
115 $relationParameters = $this->relation
->getRelationParameters();
118 * Reselect current db (needed in some cases probably due to the calling of {@link Relation})
120 $this->dbi
->selectDb(Current
::$database);
122 $rowFormat = $pmaTable->getStatusInfo('Row_format');
123 if ($pmaTable->isView()) {
124 $tableIsAView = true;
125 $tableStorageEngine = __('View');
128 $tableIsAView = false;
129 $tableStorageEngine = $pmaTable->getStorageEngine();
130 $showComment = $pmaTable->getComment();
133 $tableCollation = $pmaTable->getCollation();
134 $GLOBALS['auto_increment'] = $pmaTable->getAutoIncrement();
135 $createOptions = $pmaTable->getCreateOptions();
137 // set initial value of these variables, based on the current table engine
138 if ($pmaTable->isEngine('ARIA')) {
139 // the value for transactional can be implicit
140 // (no create option found, in this case it means 1)
141 // or explicit (option found with a value of 0 or 1)
142 // ($createOptions['transactional'] may have been set by Table class,
143 // from the $createOptions)
144 $createOptions['transactional'] = ($createOptions['transactional'] ??
'') == '0'
147 $createOptions['page_checksum'] ??
= '';
150 $pmaTable = $this->dbi
->getTable(Current
::$database, Current
::$table);
154 * If the table has to be moved to some other database
156 if ($request->hasBodyParam('submit_move') ||
$request->hasBodyParam('submit_copy')) {
157 $message = $this->operations
->moveOrCopyTable(Current
::$database, Current
::$table);
159 if (! $request->isAjax()) {
163 $this->response
->addJSON('message', $message);
165 if ($message->isSuccess()) {
166 /** @var mixed $targetDbParam */
167 $targetDbParam = $request->getParsedBodyParam('target_db');
168 if ($request->hasBodyParam('submit_move') && is_string($targetDbParam)) {
169 Current
::$database = $targetDbParam; // Used in Header::getJsParams()
172 $this->response
->addJSON('db', Current
::$database);
177 $this->response
->setRequestStatus(false);
183 $warningMessages = [];
185 * Updates table comment, type and options if required
187 if ($request->hasBodyParam('submitoptions')) {
188 /** @var mixed $newName */
189 $newName = $request->getParsedBodyParam('new_name');
190 if (is_string($newName)) {
191 if ($this->dbi
->getLowerCaseNames() === 1) {
192 $newName = mb_strtolower($newName);
195 // Get original names before rename operation
196 $oldTable = $pmaTable->getName();
197 $oldDb = $pmaTable->getDbName();
199 if ($pmaTable->rename($newName)) {
200 if ($request->getParsedBodyParam('adjust_privileges')) {
201 /** @var mixed $dbParam */
202 $dbParam = $request->getParsedBodyParam('db');
203 $this->operations
->adjustPrivilegesRenameOrMoveTable(
206 is_string($dbParam) ?
$dbParam : '',
211 // Reselect the original DB
212 Current
::$database = $oldDb;
213 $this->dbi
->selectDb($oldDb);
214 $newMessage .= $pmaTable->getLastMessage();
216 Current
::$table = $pmaTable->getName();
218 $GLOBALS['reload'] = true;
220 $newMessage .= $pmaTable->getLastError();
225 /** @var mixed $newTableStorageEngine */
226 $newTableStorageEngine = $request->getParsedBodyParam('new_tbl_storage_engine');
227 $newTblStorageEngine = '';
229 is_string($newTableStorageEngine) && $newTableStorageEngine !== ''
230 && mb_strtoupper($newTableStorageEngine) !== $tableStorageEngine
232 $newTblStorageEngine = mb_strtoupper($newTableStorageEngine);
234 if ($pmaTable->isEngine('ARIA')) {
235 $createOptions['transactional'] = ($createOptions['transactional'] ??
'')
237 $createOptions['page_checksum'] ??
= '';
241 $tableAlters = $this->operations
->getTableAltersArray(
243 $createOptions['pack_keys'],
244 (empty($createOptions['checksum']) ?
'0' : '1'),
245 ($createOptions['page_checksum'] ??
''),
246 (empty($createOptions['delay_key_write']) ?
'0' : '1'),
247 $createOptions['row_format'] ??
$pmaTable->getRowFormat(),
248 $newTblStorageEngine,
249 (isset($createOptions['transactional'])
250 && $createOptions['transactional'] == '0' ?
'0' : '1'),
255 if ($tableAlters !== []) {
256 $GLOBALS['sql_query'] = 'ALTER TABLE '
257 . Util
::backquote(Current
::$table);
258 $GLOBALS['sql_query'] .= "\r\n" . implode("\r\n", $tableAlters);
259 $GLOBALS['sql_query'] .= ';';
260 $this->dbi
->query($GLOBALS['sql_query']);
263 $warningMessages = $this->operations
->getWarningMessagesArray();
266 /** @var mixed $tableCollationParam */
267 $tableCollationParam = $request->getParsedBodyParam('tbl_collation');
269 is_string($tableCollationParam) && $tableCollationParam !== ''
270 && $request->getParsedBodyParam('change_all_collations')
272 $this->operations
->changeAllColumnsCollation(Current
::$database, Current
::$table, $tableCollationParam);
275 if ($tableCollationParam !== null && (! is_string($tableCollationParam) ||
$tableCollationParam === '')) {
276 if ($request->isAjax()) {
277 $this->response
->setRequestStatus(false);
278 $this->response
->addJSON(
280 Message
::error(__('No collation provided.')),
288 /** @var mixed $orderField */
289 $orderField = $request->getParsedBodyParam('order_field');
292 * Reordering the table has been requested by the user
294 if ($request->hasBodyParam('submitorderby') && is_string($orderField) && $orderField !== '') {
295 /** @var mixed $orderOrder */
296 $orderOrder = $request->getParsedBodyParam('order_order');
297 $GLOBALS['sql_query'] = QueryGenerator
::getQueryForReorderingTable(
299 urldecode($orderField),
300 is_string($orderOrder) ?
$orderOrder : '',
302 $this->dbi
->query($GLOBALS['sql_query']);
306 /** @var mixed $partitionOperation */
307 $partitionOperation = $request->getParsedBodyParam('partition_operation');
310 * A partition operation has been requested by the user
313 $request->hasBodyParam('submit_partition') && is_string($partitionOperation) && $partitionOperation !== ''
315 /** @var mixed $partitionNames */
316 $partitionNames = $request->getParsedBodyParam('partition_name');
317 $GLOBALS['sql_query'] = QueryGenerator
::getQueryForPartitioningTable(
320 is_array($partitionNames) ?
$partitionNames : [],
322 $this->dbi
->query($GLOBALS['sql_query']);
327 // to avoid showing the old value (for example the AUTO_INCREMENT) after
328 // a change, clear the cache
329 $this->dbi
->getCache()->clearTableCache();
330 $this->dbi
->selectDb(Current
::$database);
331 $rowFormat = $pmaTable->getStatusInfo('Row_format');
332 if ($pmaTable->isView()) {
333 $tableIsAView = true;
334 $tableStorageEngine = __('View');
337 $tableIsAView = false;
338 $tableStorageEngine = $pmaTable->getStorageEngine();
339 $showComment = $pmaTable->getComment();
342 $tableCollation = $pmaTable->getCollation();
343 $GLOBALS['auto_increment'] = $pmaTable->getAutoIncrement();
344 $createOptions = $pmaTable->getCreateOptions();
347 if (isset($result) && empty($GLOBALS['message_to_show'])) {
348 if ($newMessage === '') {
349 if (empty($GLOBALS['sql_query'])) {
350 $newMessage = Message
::success(__('No change'));
352 $newMessage = $result
357 if ($request->isAjax()) {
358 $this->response
->setRequestStatus($newMessage->isSuccess());
359 $this->response
->addJSON('message', $newMessage);
360 if (! empty($GLOBALS['sql_query'])) {
361 $this->response
->addJSON(
363 Generator
::getMessage('', $GLOBALS['sql_query']),
370 $newMessage = $result
371 ? Message
::success($newMessage)
372 : Message
::error($newMessage);
375 if ($warningMessages !== []) {
376 $newMessage = new Message();
377 $newMessage->addMessagesString($warningMessages);
378 $newMessage->setType(Message
::ERROR
);
379 if ($request->isAjax()) {
380 $this->response
->setRequestStatus(false);
381 $this->response
->addJSON('message', $newMessage);
382 if (! empty($GLOBALS['sql_query'])) {
383 $this->response
->addJSON(
385 Generator
::getMessage('', $GLOBALS['sql_query']),
393 if (empty($GLOBALS['sql_query'])) {
394 $this->response
->addHTML(
395 $newMessage->getDisplay(),
398 $this->response
->addHTML(
399 Generator
::getMessage($newMessage, $GLOBALS['sql_query']),
406 $GLOBALS['urlParams']['goto'] = $GLOBALS['urlParams']['back'] = Url
::getFromRoute('/table/operations');
408 $columns = $this->dbi
->getColumns(Current
::$database, Current
::$table);
410 $hideOrderTable = false;
411 // `ALTER TABLE ORDER BY` does not make sense for InnoDB tables that contain
412 // a user-defined clustered index (PRIMARY KEY or NOT NULL UNIQUE index).
413 // InnoDB always orders table rows according to such an index if one is present.
414 if ($tableStorageEngine === 'INNODB') {
415 $indexes = Index
::getFromTable($this->dbi
, Current
::$table, Current
::$database);
416 foreach ($indexes as $name => $idx) {
417 if ($name === 'PRIMARY') {
418 $hideOrderTable = true;
422 if ($idx->getNonUnique()) {
427 foreach ($idx->getColumns() as $column) {
428 if ($column->isNullable()) {
435 $hideOrderTable = true;
442 if (mb_strstr($showComment, '; InnoDB free') === false) {
443 if (mb_strstr($showComment, 'InnoDB free') === false) {
444 // only user entered comment
445 $comment = $showComment;
448 // remove InnoDB comment from end, just the minimal part (*? is non greedy)
449 $comment = preg_replace('@; InnoDB free:.*?$@', '', $showComment);
452 $storageEngines = StorageEngine
::getArray();
454 $charsets = Charsets
::getCharsets($this->dbi
, $config->selectedServer
['DisableIS']);
455 $collations = Charsets
::getCollations($this->dbi
, $config->selectedServer
['DisableIS']);
457 $hasPackKeys = isset($createOptions['pack_keys'])
458 && $pmaTable->isEngine(['MYISAM', 'ARIA', 'ISAM']);
459 $hasChecksumAndDelayKeyWrite = $pmaTable->isEngine(['MYISAM', 'ARIA']);
460 $hasTransactionalAndPageChecksum = $pmaTable->isEngine('ARIA');
461 $hasAutoIncrement = strlen($GLOBALS['auto_increment']) > 0
462 && $pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB']);
464 $possibleRowFormats = $this->operations
->getPossibleRowFormat();
467 $listDatabase = $this->dbi
->getDatabaseList();
468 if (count($listDatabase) <= $config->settings
['MaxDbList']) {
469 $databaseList = $listDatabase->getList();
472 $hasForeignKeys = $this->relation
->getForeigners(Current
::$database, Current
::$table, '', 'foreign') !== [];
473 $hasPrivileges = UserPrivileges
::$table && UserPrivileges
::$column && UserPrivileges
::$isReload;
474 $switchToNew = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new'];
477 $partitionsChoices = [];
479 if (Partition
::havePartitioning()) {
480 $partitionNames = Partition
::getPartitionNames(Current
::$database, Current
::$table);
481 if (isset($partitionNames[0])) {
482 $partitions = $partitionNames;
483 $partitionsChoices = $this->operations
->getPartitionMaintenanceChoices();
487 $foreigners = $this->operations
->getForeignersForReferentialIntegrityCheck(
488 $GLOBALS['urlParams'],
489 $relationParameters->relationFeature
!== null,
492 $this->render('table/operations/index', [
493 'db' => Current
::$database,
494 'table' => Current
::$table,
495 'url_params' => $GLOBALS['urlParams'],
496 'columns' => $columns,
497 'hide_order_table' => $hideOrderTable,
498 'table_comment' => $comment,
499 'storage_engine' => $tableStorageEngine,
500 'storage_engines' => $storageEngines,
501 'charsets' => $charsets,
502 'collations' => $collations,
503 'tbl_collation' => $tableCollation,
504 'row_formats' => $possibleRowFormats[$tableStorageEngine] ??
[],
505 'row_format_current' => $rowFormat,
506 'has_auto_increment' => $hasAutoIncrement,
507 'auto_increment' => $GLOBALS['auto_increment'],
508 'has_pack_keys' => $hasPackKeys,
509 'pack_keys' => $createOptions['pack_keys'] ??
'',
510 'has_transactional_and_page_checksum' => $hasTransactionalAndPageChecksum,
511 'has_checksum_and_delay_key_write' => $hasChecksumAndDelayKeyWrite,
512 'delay_key_write' => empty($createOptions['delay_key_write']) ?
'0' : '1',
513 'transactional' => ($createOptions['transactional'] ??
'') == '0' ?
'0' : '1',
514 'page_checksum' => $createOptions['page_checksum'] ??
'',
515 'checksum' => empty($createOptions['checksum']) ?
'0' : '1',
516 'database_list' => $databaseList,
517 'has_foreign_keys' => $hasForeignKeys,
518 'has_privileges' => $hasPrivileges,
519 'switch_to_new' => $switchToNew,
520 'is_system_schema' => $isSystemSchema,
521 'is_view' => $tableIsAView,
522 'partitions' => $partitions,
523 'partitions_choices' => $partitionsChoices,
524 'foreigners' => $foreigners,