src/Operations.php
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Database\Events;
use PhpMyAdmin\Database\Routines;
use PhpMyAdmin\Engines\Innodb;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Partitioning\Partition;
use PhpMyAdmin\Plugins\Export\ExportSql;
use PhpMyAdmin\Table\MoveMode;
use PhpMyAdmin\Table\MoveScope;
use PhpMyAdmin\Table\Table;
use PhpMyAdmin\Table\TableMover;
use PhpMyAdmin\Triggers\Triggers;
use function __;
use function array_merge;
use function count;
use function explode;
use function in_array;
use function is_scalar;
use function is_string;
use function mb_strtolower;
use function str_replace;
use function strtolower;
use function urldecode;
/**
* Set of functions with the operations section in phpMyAdmin
*/
class Operations
{
public function __construct(
private readonly DatabaseInterface $dbi,
private readonly Relation $relation,
private readonly TableMover $tableMover,
) {
}
/**
* Run the Procedure definitions and function definitions
*
* to avoid selecting alternatively the current and new db
* we would need to modify the CREATE definitions to qualify
* the db name
*
* @param string $db database name
*/
public function runProcedureAndFunctionDefinitions(string $db, DatabaseName $newDatabaseName): void
{
foreach (Routines::getProcedureNames($this->dbi, $db) as $procedureName) {
$this->dbi->selectDb($db);
$query = Routines::getProcedureDefinition($this->dbi, $db, $procedureName);
if ($query === null) {
continue;
}
// collect for later display
$GLOBALS['sql_query'] .= "\n" . $query;
$this->dbi->selectDb($newDatabaseName);
$this->dbi->query($query);
}
foreach (Routines::getFunctionNames($this->dbi, $db) as $functionName) {
$this->dbi->selectDb($db);
$query = Routines::getFunctionDefinition($this->dbi, $db, $functionName);
if ($query === null) {
continue;
}
// collect for later display
$GLOBALS['sql_query'] .= "\n" . $query;
$this->dbi->selectDb($newDatabaseName);
$this->dbi->query($query);
}
}
/**
* Create database before copy
*/
public function createDbBeforeCopy(UserPrivileges $userPrivileges, DatabaseName $newDatabaseName): void
{
$localQuery = 'CREATE DATABASE IF NOT EXISTS '
. Util::backquote($newDatabaseName);
if (isset($_POST['db_collation'])) {
$localQuery .= ' DEFAULT'
. Util::getCharsetQueryPart($_POST['db_collation']);
}
$localQuery .= ';';
$GLOBALS['sql_query'] .= $localQuery;
// save the original db name because Tracker.php which
// may be called under $this->dbi->query() changes \PhpMyAdmin\Current::$database
// for some statements, one of which being CREATE DATABASE
$originalDb = Current::$database;
$this->dbi->query($localQuery);
Current::$database = $originalDb;
// Set the SQL mode to NO_AUTO_VALUE_ON_ZERO to prevent MySQL from creating
// export statements it cannot import
$sqlSetMode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
$this->dbi->query($sqlSetMode);
// rebuild the database list because Table::moveCopy
// checks in this list if the target db exists
$this->dbi->getDatabaseList()->build($userPrivileges);
}
/**
* Get views as an array and create SQL view stand-in
*
* @param string[] $tables array of all tables in given db or dbs
* @param ExportSql $exportSqlPlugin export plugin instance
* @param string $db database name
*
* @return string[]
*/
public function getViewsAndCreateSqlViewStandIn(
array $tables,
ExportSql $exportSqlPlugin,
string $db,
DatabaseName $newDatabaseName,
): array {
$views = [];
foreach ($tables as $table) {
// to be able to rename a db containing views,
// first all the views are collected and a stand-in is created
// the real views are created after the tables
if (! $this->dbi->getTable($db, $table)->isView()) {
continue;
}
// If view exists, and 'add drop view' is selected: Drop it!
if ($_POST['what'] !== 'nocopy' && isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true') {
$dropQuery = 'DROP VIEW IF EXISTS '
. Util::backquote($newDatabaseName) . '.'
. Util::backquote($table);
$this->dbi->query($dropQuery);
$GLOBALS['sql_query'] .= "\n" . $dropQuery . ';';
}
$views[] = $table;
// Create stand-in definition to resolve view dependencies
$sqlViewStandin = $exportSqlPlugin->getTableDefStandIn($db, $table);
$this->dbi->selectDb($newDatabaseName);
$this->dbi->query($sqlViewStandin);
$GLOBALS['sql_query'] .= "\n" . $sqlViewStandin;
}
return $views;
}
/**
* Get sql query for copy/rename table and boolean for whether copy/rename or not
*
* @param string[] $tables array of all tables in given db or dbs
* @param bool $move whether database name is empty or not
* @param string $db database name
*
* @return mixed[] SQL queries for the constraints
*/
public function copyTables(array $tables, bool $move, string $db, DatabaseName $newDatabaseName): array
{
$sqlContraints = [];
foreach ($tables as $table) {
// skip the views; we have created stand-in definitions
if ($this->dbi->getTable($db, $table)->isView()) {
continue;
}
// value of $what for this table only
$copyMode = $_POST['what'];
// do not copy the data from a Merge table
// note: on the calling FORM, 'data' means 'structure and data'
if ($this->dbi->getTable($db, $table)->isMerge()) {
if ($copyMode === 'data') {
$copyMode = 'structure';
}
if ($copyMode === 'dataonly') {
$copyMode = 'nocopy';
}
}
if ($copyMode === 'nocopy') {
continue;
}
// keep the triggers from the original db+table
// (third param is empty because delimiters are only intended
// for importing via the mysql client or our Import feature)
$triggers = Triggers::getDetails($this->dbi, $db, $table);
$moveScope = MoveScope::tryFrom($copyMode) ?? MoveScope::StructureAndData;
if (
! $this->tableMover->moveCopy(
$db,
$table,
$newDatabaseName->getName(),
$table,
$move ? MoveScope::Move : $moveScope,
MoveMode::WholeDatabase,
isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
)
) {
$GLOBALS['_error'] = true;
break;
}
// apply the triggers to the destination db+table
if ($triggers !== []) {
$this->dbi->selectDb($newDatabaseName);
foreach ($triggers as $trigger) {
$createSqlQuery = $trigger->getCreateSql('');
$this->dbi->query($createSqlQuery);
$GLOBALS['sql_query'] .= "\n" . $createSqlQuery . ';';
}
}
// this does not apply to a rename operation
if (! isset($_POST['add_constraints']) || $this->tableMover->sqlConstraintsQuery === '') {
continue;
}
$sqlContraints[] = $this->tableMover->sqlConstraintsQuery;
$this->tableMover->sqlConstraintsQuery = '';
}
return $sqlContraints;
}
/**
* Run the EVENT definition for selected database
*
* to avoid selecting alternatively the current and new db
* we would need to modify the CREATE definitions to qualify
* the db name
*
* @param string $db database name
*/
public function runEventDefinitionsForDb(string $db, DatabaseName $newDatabaseName): void
{
/** @var string[] $eventNames */
$eventNames = $this->dbi->fetchResult(
'SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA= '
. $this->dbi->quoteString($db) . ';',
);
foreach ($eventNames as $eventName) {
$this->dbi->selectDb($db);
$query = Events::getDefinition($this->dbi, $db, $eventName);
// collect for later display
$GLOBALS['sql_query'] .= "\n" . $query;
$this->dbi->selectDb($newDatabaseName);
$this->dbi->query($query);
}
}
/**
* Handle the views, return the boolean value whether table rename/copy or not
*
* @param string[] $views views as an array
* @param bool $move whether database name is empty or not
* @param string $db database name
*/
public function handleTheViews(array $views, bool $move, string $db, DatabaseName $newDatabaseName): void
{
// Add DROP IF EXIST to CREATE VIEW query, to remove stand-in VIEW that was created earlier.
foreach ($views as $view) {
$copyingSucceeded = $this->tableMover->moveCopy(
$db,
$view,
$newDatabaseName->getName(),
$view,
$move ? MoveScope::Move : MoveScope::StructureOnly,
MoveMode::WholeDatabase,
true,
);
if (! $copyingSucceeded) {
$GLOBALS['_error'] = true;
break;
}
}
}
/**
* Adjust the privileges after Renaming the db
*
* @param string $oldDb Database name before renaming
*/
public function adjustPrivilegesMoveDb(
UserPrivileges $userPrivileges,
string $oldDb,
DatabaseName $newDatabaseName,
): void {
if (
! $userPrivileges->database || ! $userPrivileges->table
|| ! $userPrivileges->column || ! $userPrivileges->routines
|| ! $userPrivileges->isReload
) {
return;
}
$this->dbi->selectDb('mysql');
$newName = str_replace('_', '\_', $newDatabaseName->getName());
$oldDb = str_replace('_', '\_', $oldDb);
// For Db specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('db')
. 'SET Db = ' . $this->dbi->quoteString($newName)
. ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
// For table specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
. 'SET Db = ' . $this->dbi->quoteString($newName)
. ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
// For column specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
. 'SET Db = ' . $this->dbi->quoteString($newName)
. ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
// For procedures specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('procs_priv')
. 'SET Db = ' . $this->dbi->quoteString($newName)
. ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
// Finally FLUSH the new privileges
$this->dbi->query('FLUSH PRIVILEGES;');
}
/**
* Adjust the privileges after Copying the db
*
* @param string $oldDb Database name before copying
*/
public function adjustPrivilegesCopyDb(
UserPrivileges $userPrivileges,
string $oldDb,
DatabaseName $newDatabaseName,
): void {
if (
! $userPrivileges->database || ! $userPrivileges->table
|| ! $userPrivileges->column || ! $userPrivileges->routines
|| ! $userPrivileges->isReload
) {
return;
}
$this->dbi->selectDb('mysql');
$newName = str_replace('_', '\_', $newDatabaseName->getName());
$oldDb = str_replace('_', '\_', $oldDb);
$queryDbSpecificOld = 'SELECT * FROM '
. Util::backquote('db') . ' WHERE '
. 'Db = "' . $oldDb . '";';
$oldPrivsDb = $this->dbi->fetchResult($queryDbSpecificOld, 0);
foreach ($oldPrivsDb as $oldPriv) {
$newDbDbPrivsQuery = 'INSERT INTO ' . Util::backquote('db')
. ' VALUES("' . $oldPriv[0] . '", "' . $newName . '"';
$privCount = count($oldPriv);
for ($i = 2; $i < $privCount; $i++) {
$newDbDbPrivsQuery .= ', "' . $oldPriv[$i] . '"';
}
$newDbDbPrivsQuery .= ')';
$this->dbi->query($newDbDbPrivsQuery);
}
// For Table Specific privileges
$queryTableSpecificOld = 'SELECT * FROM '
. Util::backquote('tables_priv') . ' WHERE '
. 'Db = "' . $oldDb . '";';
$oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
foreach ($oldPrivsTable as $oldPriv) {
$newDbTablePrivsQuery = 'INSERT INTO ' . Util::backquote(
'tables_priv',
) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
. $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
. '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
. $oldPriv[7] . '");';
$this->dbi->query($newDbTablePrivsQuery);
}
// For Column Specific privileges
$queryColSpecificOld = 'SELECT * FROM '
. Util::backquote('columns_priv') . ' WHERE '
. 'Db = "' . $oldDb . '";';
$oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
foreach ($oldPrivsCol as $oldPriv) {
$newDbColPrivsQuery = 'INSERT INTO ' . Util::backquote(
'columns_priv',
) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
. $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
. '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '");';
$this->dbi->query($newDbColPrivsQuery);
}
// For Procedure Specific privileges
$queryProcSpecificOld = 'SELECT * FROM '
. Util::backquote('procs_priv') . ' WHERE '
. 'Db = "' . $oldDb . '";';
$oldPrivsProc = $this->dbi->fetchResult($queryProcSpecificOld, 0);
foreach ($oldPrivsProc as $oldPriv) {
$newDbProcPrivsQuery = 'INSERT INTO ' . Util::backquote(
'procs_priv',
) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
. $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
. '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
. $oldPriv[7] . '");';
$this->dbi->query($newDbProcPrivsQuery);
}
// Finally FLUSH the new privileges
$this->dbi->query('FLUSH PRIVILEGES;');
}
/**
* Create all accumulated constraints
*
* @param mixed[] $sqlConstraints array of sql constraints for the database
*/
public function createAllAccumulatedConstraints(array $sqlConstraints, DatabaseName $newDatabaseName): void
{
$this->dbi->selectDb($newDatabaseName);
foreach ($sqlConstraints as $query) {
$this->dbi->query($query);
// and prepare to display them
$GLOBALS['sql_query'] .= "\n" . $query;
}
}
/**
* Duplicate the bookmarks for the db (done once for each db)
*
* @param bool $error whether table rename/copy or not
* @param string $db database name
*/
public function duplicateBookmarks(bool $error, string $db, DatabaseName $newDatabaseName): void
{
if ($error || $db === $newDatabaseName->getName()) {
return;
}
$getFields = ['user', 'label', 'query'];
$whereFields = ['dbase' => $db];
$newFields = ['dbase' => $newDatabaseName->getName()];
$this->tableMover->duplicateInfo('bookmarkwork', 'bookmark', $getFields, $whereFields, $newFields);
}
/**
* Get array of possible row formats
*
* @return string[][]
*/
public function getPossibleRowFormat(): array
{
// the outer array is for engines, the inner array contains the dropdown
// option values as keys then the dropdown option labels
$possibleRowFormats = [
'ARCHIVE' => ['COMPRESSED' => 'COMPRESSED'],
'ARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
'MARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
'MYISAM' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
'PBXT' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
'INNODB' => ['COMPACT' => 'COMPACT', 'REDUNDANT' => 'REDUNDANT'],
];
/** @var Innodb $innodbEnginePlugin */
$innodbEnginePlugin = StorageEngine::getEngine('Innodb');
$innodbPluginVersion = $innodbEnginePlugin->getInnodbPluginVersion();
$innodbFileFormat = '';
if ($innodbPluginVersion !== '') {
$innodbFileFormat = $innodbEnginePlugin->getInnodbFileFormat() ?? '';
}
/**
* Newer MySQL/MariaDB always return empty a.k.a '' on $innodbFileFormat otherwise
* old versions of MySQL/MariaDB must be returning something or not empty.
* This patch is to support newer MySQL/MariaDB while also for backward compatibilities.
*/
if (
(strtolower($innodbFileFormat) === 'barracuda') || ($innodbFileFormat == '')
&& $innodbEnginePlugin->supportsFilePerTable()
) {
$possibleRowFormats['INNODB']['DYNAMIC'] = 'DYNAMIC';
$possibleRowFormats['INNODB']['COMPRESSED'] = 'COMPRESSED';
}
return $possibleRowFormats;
}
/** @return array<string, string> */
public function getPartitionMaintenanceChoices(): array
{
$choices = [
'ANALYZE' => __('Analyze'),
'CHECK' => __('Check'),
'OPTIMIZE' => __('Optimize'),
'REBUILD' => __('Rebuild'),
'REPAIR' => __('Repair'),
'TRUNCATE' => __('Truncate'),
];
$partitionMethod = Partition::getPartitionMethod(Current::$database, Current::$table);
// add COALESCE or DROP option to choices array depending on Partition method
if (
$partitionMethod === 'RANGE'
|| $partitionMethod === 'RANGE COLUMNS'
|| $partitionMethod === 'LIST'
|| $partitionMethod === 'LIST COLUMNS'
) {
$choices['DROP'] = __('Drop');
} else {
$choices['COALESCE'] = __('Coalesce');
}
return $choices;
}
/**
* @param mixed[] $urlParams Array of url parameters.
* @param bool $hasRelationFeature If relation feature is enabled.
*
* @return mixed[]
*/
public function getForeignersForReferentialIntegrityCheck(
array $urlParams,
bool $hasRelationFeature,
): array {
if (! $hasRelationFeature) {
return [];
}
$foreigners = [];
$this->dbi->selectDb(Current::$database);
$foreign = $this->relation->getForeigners(Current::$database, Current::$table, '', 'internal');
foreach ($foreign as $master => $arr) {
$joinQuery = 'SELECT '
. Util::backquote(Current::$table) . '.*'
. ' FROM ' . Util::backquote(Current::$table)
. ' LEFT JOIN '
. Util::backquote($arr['foreign_db'])
. '.'
. Util::backquote($arr['foreign_table']);
if ($arr['foreign_table'] == Current::$table) {
$foreignTable = Current::$table . '1';
$joinQuery .= ' AS ' . Util::backquote($foreignTable);
} else {
$foreignTable = $arr['foreign_table'];
}
$joinQuery .= ' ON '
. Util::backquote(Current::$table) . '.'
. Util::backquote($master)
. ' = '
. Util::backquote($arr['foreign_db'])
. '.'
. Util::backquote($foreignTable) . '.'
. Util::backquote($arr['foreign_field'])
. ' WHERE '
. Util::backquote($arr['foreign_db'])
. '.'
. Util::backquote($foreignTable) . '.'
. Util::backquote($arr['foreign_field'])
. ' IS NULL AND '
. Util::backquote(Current::$table) . '.'
. Util::backquote($master)
. ' IS NOT NULL';
$thisUrlParams = array_merge(
$urlParams,
['sql_query' => $joinQuery, 'sql_signature' => Core::signSqlQuery($joinQuery)],
);
$foreigners[] = [
'params' => $thisUrlParams,
'master' => $master,
'db' => $arr['foreign_db'],
'table' => $arr['foreign_table'],
'field' => $arr['foreign_field'],
];
}
return $foreigners;
}
/**
* Get table alters array
*
* @param Table $pmaTable The Table object
* @param string $packKeys pack keys
* @param string $checksum value of checksum
* @param string $pageChecksum value of page checksum
* @param string $delayKeyWrite delay key write
* @param string $rowFormat row format
* @param string $newTblStorageEngine table storage engine
* @param string $transactional value of transactional
* @param string $tableCollation collation of the table
*
* @return string[]
*/
public function getTableAltersArray(
Table $pmaTable,
string $packKeys,
string $checksum,
string $pageChecksum,
string $delayKeyWrite,
string $rowFormat,
string $newTblStorageEngine,
string $transactional,
string $tableCollation,
string $tableStorageEngine,
): array {
$GLOBALS['auto_increment'] ??= null;
$tableAlters = [];
if (isset($_POST['comment']) && urldecode($_POST['prev_comment']) !== $_POST['comment']) {
$tableAlters[] = 'COMMENT = ' . $this->dbi->quoteString($_POST['comment']);
}
if (
$newTblStorageEngine !== ''
&& mb_strtolower($newTblStorageEngine) !== mb_strtolower($tableStorageEngine)
) {
$tableAlters[] = 'ENGINE = ' . $newTblStorageEngine;
}
if (! empty($_POST['tbl_collation']) && $_POST['tbl_collation'] !== $tableCollation) {
$tableAlters[] = 'DEFAULT '
. Util::getCharsetQueryPart($_POST['tbl_collation']);
}
if (
$pmaTable->isEngine(['MYISAM', 'ARIA', 'ISAM'])
&& isset($_POST['new_pack_keys'])
&& $_POST['new_pack_keys'] != $packKeys
) {
$tableAlters[] = 'pack_keys = ' . $_POST['new_pack_keys'];
}
$newChecksum = empty($_POST['new_checksum']) ? '0' : '1';
if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newChecksum !== $checksum) {
$tableAlters[] = 'checksum = ' . $newChecksum;
}
$newTransactional = empty($_POST['new_transactional']) ? '0' : '1';
if ($pmaTable->isEngine('ARIA') && $newTransactional !== $transactional) {
$tableAlters[] = 'TRANSACTIONAL = ' . $newTransactional;
}
$newPageChecksum = empty($_POST['new_page_checksum']) ? '0' : '1';
if ($pmaTable->isEngine('ARIA') && $newPageChecksum !== $pageChecksum) {
$tableAlters[] = 'PAGE_CHECKSUM = ' . $newPageChecksum;
}
$newDelayKeyWrite = empty($_POST['new_delay_key_write']) ? '0' : '1';
if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newDelayKeyWrite !== $delayKeyWrite) {
$tableAlters[] = 'delay_key_write = ' . $newDelayKeyWrite;
}
if (
$pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB'])
&& ! empty($_POST['new_auto_increment'])
&& (! isset($GLOBALS['auto_increment'])
|| $_POST['new_auto_increment'] !== $GLOBALS['auto_increment'])
&& $_POST['new_auto_increment'] !== $_POST['hidden_auto_increment']
) {
$tableAlters[] = 'auto_increment = ' . (int) $_POST['new_auto_increment'];
}
if (! empty($_POST['new_row_format'])) {
$newRowFormat = $_POST['new_row_format'];
$newRowFormatLower = mb_strtolower($newRowFormat);
if (
$pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT'])
&& ($rowFormat === '' || $newRowFormatLower !== mb_strtolower($rowFormat))
&& in_array($newRowFormat, ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT'], true)
) {
$tableAlters[] = 'ROW_FORMAT = ' . $newRowFormat;
}
}
return $tableAlters;
}
/**
* Get warning messages array
*
* @return string[]
*/
public function getWarningMessagesArray(mixed $newTableStorageEngine): array
{
$warningMessages = [];
foreach ($this->dbi->getWarnings() as $warning) {
// In MariaDB 5.1.44, when altering a table from Maria to MyISAM
// and if TRANSACTIONAL was set, the system reports an error;
// I discussed with a Maria developer and he agrees that this
// should not be reported with a Level of Error, so here
// I just ignore it. But there are other 1478 messages
// that it's better to show.
if (
$newTableStorageEngine === 'MyISAM'
&& $warning->code === 1478
&& $warning->level === 'Error'
) {
continue;
}
$warningMessages[] = (string) $warning;
}
return $warningMessages;
}
/**
* Adjust the privileges after renaming/moving a table
*
* @param string $oldDb Database name before table renaming/moving table
* @param string $oldTable Table name before table renaming/moving table
* @param string $newDb Database name after table renaming/ moving table
* @param string $newTable Table name after table renaming/moving table
*/
public function adjustPrivilegesRenameOrMoveTable(
UserPrivileges $userPrivileges,
string $oldDb,
string $oldTable,
string $newDb,
string $newTable,
): void {
if (! $userPrivileges->table || ! $userPrivileges->column || ! $userPrivileges->isReload) {
return;
}
$this->dbi->selectDb('mysql');
// For table specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
. 'SET Db = ' . $this->dbi->quoteString($newDb)
. ', Table_name = ' . $this->dbi->quoteString($newTable)
. ' where Db = ' . $this->dbi->quoteString($oldDb)
. ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
. ';');
// For column specific privileges
$this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
. 'SET Db = ' . $this->dbi->quoteString($newDb)
. ', Table_name = ' . $this->dbi->quoteString($newTable)
. ' where Db = ' . $this->dbi->quoteString($oldDb)
. ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
. ';');
// Finally FLUSH the new privileges
$this->dbi->query('FLUSH PRIVILEGES;');
}
/**
* Adjust the privileges after copying a table
*
* @param string $oldDb Database name before table copying
* @param string $oldTable Table name before table copying
* @param string $newDb Database name after table copying
* @param string $newTable Table name after table copying
*/
public function adjustPrivilegesCopyTable(
UserPrivileges $userPrivileges,
string $oldDb,
string $oldTable,
string $newDb,
string $newTable,
): void {
if (! $userPrivileges->table || ! $userPrivileges->column || ! $userPrivileges->isReload) {
return;
}
$this->dbi->selectDb('mysql');
// For Table Specific privileges
$queryTableSpecificOld = 'SELECT * FROM '
. Util::backquote('tables_priv') . ' where '
. 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
$oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
foreach ($oldPrivsTable as $oldPriv) {
$newDbTablePrivsQuery = 'INSERT INTO '
. Util::backquote('tables_priv') . ' VALUES("'
. $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
. $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
. '", "' . $oldPriv[6] . '", "' . $oldPriv[7] . '");';
$this->dbi->query($newDbTablePrivsQuery);
}
// For Column Specific privileges
$queryColSpecificOld = 'SELECT * FROM '
. Util::backquote('columns_priv') . ' WHERE '
. 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
$oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
foreach ($oldPrivsCol as $oldPriv) {
$newDbColPrivsQuery = 'INSERT INTO '
. Util::backquote('columns_priv') . ' VALUES("'
. $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
. $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
. '", "' . $oldPriv[6] . '");';
$this->dbi->query($newDbColPrivsQuery);
}
// Finally FLUSH the new privileges
$this->dbi->query('FLUSH PRIVILEGES;');
}
/**
* Change all collations and character sets of all columns in table
*
* @param string $db Database name
* @param string $table Table name
* @param string $tableCollation Collation Name
*/
public function changeAllColumnsCollation(string $db, string $table, string $tableCollation): void
{
$this->dbi->selectDb($db);
$changeAllCollationsQuery = 'ALTER TABLE '
. Util::backquote($table)
. ' CONVERT TO';
[$charset] = explode('_', $tableCollation);
$changeAllCollationsQuery .= ' CHARACTER SET ' . $charset
. ($charset === $tableCollation ? '' : ' COLLATE ' . $tableCollation);
$this->dbi->query($changeAllCollationsQuery);
}
/**
* Move or copy a table
*
* @param string $db current database name
* @param string $table current table name
*/
public function moveOrCopyTable(UserPrivileges $userPrivileges, string $db, string $table): Message
{
/**
* Selects the database to work with
*/
$this->dbi->selectDb($db);
/**
* $_POST['target_db'] could be empty in case we came from an input field
* (when there are many databases, no drop-down)
*/
$targetDb = $db;
if (isset($_POST['target_db']) && is_string($_POST['target_db']) && $_POST['target_db'] !== '') {
$targetDb = $_POST['target_db'];
}
/**
* A target table name has been sent to this script -> do the work
*/
if (isset($_POST['new_name']) && is_scalar($_POST['new_name']) && (string) $_POST['new_name'] !== '') {
if ($db === $targetDb && $table == $_POST['new_name']) {
if (isset($_POST['submit_move'])) {
$message = Message::error(__('Can\'t move table to same one!'));
} else {
$message = Message::error(__('Can\'t copy table to same one!'));
}
} else {
$move = isset($_POST['submit_move']);
$this->tableMover->moveCopy(
$db,
$table,
$targetDb,
(string) $_POST['new_name'],
$move ? MoveScope::Move : MoveScope::from($_POST['what']),
MoveMode::SingleTable,
isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
);
if (! empty($_POST['adjust_privileges'])) {
if (isset($_POST['submit_move'])) {
$this->adjustPrivilegesRenameOrMoveTable(
$userPrivileges,
$db,
$table,
$targetDb,
(string) $_POST['new_name'],
);
} else {
$this->adjustPrivilegesCopyTable(
$userPrivileges,
$db,
$table,
$targetDb,
(string) $_POST['new_name'],
);
}
if (isset($_POST['submit_move'])) {
$message = Message::success(
__(
'Table %s has been moved to %s. Privileges have been adjusted.',
),
);
} else {
$message = Message::success(
__(
'Table %s has been copied to %s. Privileges have been adjusted.',
),
);
}
} elseif (isset($_POST['submit_move'])) {
$message = Message::success(
__('Table %s has been moved to %s.'),
);
} else {
$message = Message::success(
__('Table %s has been copied to %s.'),
);
}
$old = Util::backquote($db) . '.'
. Util::backquote($table);
$message->addParam($old);
$newName = (string) $_POST['new_name'];
if ($this->dbi->getLowerCaseNames() === 1) {
$newName = strtolower($newName);
}
Current::$table = $newName;
$new = Util::backquote($targetDb) . '.'
. Util::backquote($newName);
$message->addParam($new);
}
} else {
/**
* No new name for the table!
*/
$message = Message::error(__('The table name is empty!'));
}
return $message;
}
}