Extract privileges globals to static props of UserPrivileges class
[phpmyadmin.git] / src / Controllers / Operations / TableController.php
blob0b14e0187ea764a4924e8b4f4b2c1e040f011070
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Controllers\Operations;
7 use PhpMyAdmin\Charsets;
8 use PhpMyAdmin\CheckUserPrivileges;
9 use PhpMyAdmin\Config;
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;
19 use PhpMyAdmin\Index;
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;
28 use PhpMyAdmin\Url;
29 use PhpMyAdmin\UserPrivileges;
30 use PhpMyAdmin\Util;
32 use function __;
33 use function count;
34 use function implode;
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;
41 use function strlen;
42 use function urldecode;
44 class TableController extends AbstractController
46 public function __construct(
47 ResponseRenderer $response,
48 Template $template,
49 private Operations $operations,
50 private CheckUserPrivileges $checkUserPrivileges,
51 private Relation $relation,
52 private DatabaseInterface $dbi,
53 private readonly DbTableExists $dbTableExists,
54 ) {
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'])) {
76 return;
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.')));
91 return;
94 $this->redirect('/', ['reload' => true, 'message' => __('No databases selected.')]);
96 return;
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.')));
105 return;
108 $this->redirect('/', ['reload' => true, 'message' => __('No table selected.')]);
110 return;
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');
126 $showComment = '';
127 } else {
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'
145 ? '0'
146 : '1';
147 $createOptions['page_checksum'] ??= '';
150 $pmaTable = $this->dbi->getTable(Current::$database, Current::$table);
151 $rereadInfo = false;
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()) {
160 return;
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);
174 return;
177 $this->response->setRequestStatus(false);
179 return;
182 $newMessage = '';
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(
204 $oldDb,
205 $oldTable,
206 is_string($dbParam) ? $dbParam : '',
207 $newName,
211 // Reselect the original DB
212 Current::$database = $oldDb;
213 $this->dbi->selectDb($oldDb);
214 $newMessage .= $pmaTable->getLastMessage();
215 $result = true;
216 Current::$table = $pmaTable->getName();
217 $rereadInfo = true;
218 $GLOBALS['reload'] = true;
219 } else {
220 $newMessage .= $pmaTable->getLastError();
221 $result = false;
225 /** @var mixed $newTableStorageEngine */
226 $newTableStorageEngine = $request->getParsedBodyParam('new_tbl_storage_engine');
227 $newTblStorageEngine = '';
228 if (
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'] ?? '')
236 == '0' ? '0' : '1';
237 $createOptions['page_checksum'] ??= '';
241 $tableAlters = $this->operations->getTableAltersArray(
242 $pmaTable,
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'),
251 $tableCollation,
252 $tableStorageEngine,
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']);
261 $result = true;
262 $rereadInfo = true;
263 $warningMessages = $this->operations->getWarningMessagesArray();
266 /** @var mixed $tableCollationParam */
267 $tableCollationParam = $request->getParsedBodyParam('tbl_collation');
268 if (
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(
279 'message',
280 Message::error(__('No collation provided.')),
283 return;
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(
298 Current::$table,
299 urldecode($orderField),
300 is_string($orderOrder) ? $orderOrder : '',
302 $this->dbi->query($GLOBALS['sql_query']);
303 $result = true;
306 /** @var mixed $partitionOperation */
307 $partitionOperation = $request->getParsedBodyParam('partition_operation');
310 * A partition operation has been requested by the user
312 if (
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(
318 Current::$table,
319 $partitionOperation,
320 is_array($partitionNames) ? $partitionNames : [],
322 $this->dbi->query($GLOBALS['sql_query']);
323 $result = true;
326 if ($rereadInfo) {
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');
335 $showComment = '';
336 } else {
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'));
351 } else {
352 $newMessage = $result
353 ? Message::success()
354 : Message::error();
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(
362 'sql_query',
363 Generator::getMessage('', $GLOBALS['sql_query']),
367 return;
369 } else {
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(
384 'sql_query',
385 Generator::getMessage('', $GLOBALS['sql_query']),
389 return;
393 if (empty($GLOBALS['sql_query'])) {
394 $this->response->addHTML(
395 $newMessage->getDisplay(),
397 } else {
398 $this->response->addHTML(
399 Generator::getMessage($newMessage, $GLOBALS['sql_query']),
403 unset($newMessage);
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;
419 break;
422 if ($idx->getNonUnique()) {
423 continue;
426 $notNull = true;
427 foreach ($idx->getColumns() as $column) {
428 if ($column->isNullable()) {
429 $notNull = false;
430 break;
434 if ($notNull) {
435 $hideOrderTable = true;
436 break;
441 $comment = '';
442 if (mb_strstr($showComment, '; InnoDB free') === false) {
443 if (mb_strstr($showComment, 'InnoDB free') === false) {
444 // only user entered comment
445 $comment = $showComment;
447 } else {
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();
466 $databaseList = [];
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'];
476 $partitions = [];
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,