Commit 6682ec9a authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski
Browse files

Trigger warnings on MyISAM tables creation

parent 2a2da6c6
...@@ -56,6 +56,7 @@ The present file will list all changes made to the project; according to the ...@@ -56,6 +56,7 @@ The present file will list all changes made to the project; according to the
- `Transfer::transferDropdownNetpoint()` has been renamed to `Transfer::transferDropdownSocket()`. - `Transfer::transferDropdownNetpoint()` has been renamed to `Transfer::transferDropdownSocket()`.
#### Deprecated #### Deprecated
- Usage of `MyISAM` engine in database, in favor of `InnoDB` engine.
- Usage of `utf8mb3` charset/collation in database in favor of `utf8mb4` charset/collation. - Usage of `utf8mb3` charset/collation in database in favor of `utf8mb4` charset/collation.
- Handling of encoded/escaped value in `autoName()` - Handling of encoded/escaped value in `autoName()`
- `Netpoint` has been deprecated and replaced by `Socket` - `Netpoint` has been deprecated and replaced by `Socket`
......
...@@ -187,6 +187,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force ...@@ -187,6 +187,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
* @param OutputInterface $output * @param OutputInterface $output
* @param bool $auto_config_flags * @param bool $auto_config_flags
* @param bool $use_utf8mb4 * @param bool $use_utf8mb4
* @param bool $allow_myisam
* *
* @throws InvalidArgumentException * @throws InvalidArgumentException
* *
...@@ -196,7 +197,8 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force ...@@ -196,7 +197,8 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
InputInterface $input, InputInterface $input,
OutputInterface $output, OutputInterface $output,
bool $auto_config_flags = true, bool $auto_config_flags = true,
bool $use_utf8mb4 = false bool $use_utf8mb4 = false,
bool $allow_myisam = true
) { ) {
$db_pass = $input->getOption('db-password'); $db_pass = $input->getOption('db-password');
...@@ -275,6 +277,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force ...@@ -275,6 +277,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
}; };
$config_flags = $db->getComputedConfigBooleanFlags(); $config_flags = $db->getComputedConfigBooleanFlags();
$use_utf8mb4 = $config_flags[DBConnection::PROPERTY_USE_UTF8MB4] ?? $use_utf8mb4; $use_utf8mb4 = $config_flags[DBConnection::PROPERTY_USE_UTF8MB4] ?? $use_utf8mb4;
$allow_myisam = $config_flags[DBConnection::PROPERTY_ALLOW_MYISAM] ?? $allow_myisam;
} }
DBConnection::setConnectionCharset($mysqli, $use_utf8mb4); DBConnection::setConnectionCharset($mysqli, $use_utf8mb4);
...@@ -285,7 +288,16 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force ...@@ -285,7 +288,16 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
'<comment>' . __('Saving configuration file...') . '</comment>', '<comment>' . __('Saving configuration file...') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE OutputInterface::VERBOSITY_VERBOSE
); );
if (!DBConnection::createMainConfig($db_hostport, $db_user, $db_pass, $db_name, $log_deprecation_warnings, $use_utf8mb4)) { $result = DBConnection::createMainConfig(
$db_hostport,
$db_user,
$db_pass,
$db_name,
$log_deprecation_warnings,
$use_utf8mb4,
$allow_myisam
);
if (!$result) {
$message = sprintf( $message = sprintf(
__('Cannot write configuration file "%s".'), __('Cannot write configuration file "%s".'),
GLPI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'config_db.php' GLPI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'config_db.php'
...@@ -298,13 +310,30 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force ...@@ -298,13 +310,30 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
} }
// Set $db instance to use new connection properties // Set $db instance to use new connection properties
$this->db = new class($db_hostport, $db_user, $db_pass, $db_name, $log_deprecation_warnings, $use_utf8mb4) extends DBmysql { $this->db = new class(
public function __construct($dbhost, $dbuser, $dbpassword, $dbdefault, $log_deprecation_warnings, $use_utf8mb4) { $db_hostport,
$this->dbhost = $dbhost; $db_user,
$this->dbuser = $dbuser; $db_pass,
$this->dbpassword = $dbpassword; $db_name,
$this->dbdefault = $dbdefault; $log_deprecation_warnings,
$this->use_utf8mb4 = $use_utf8mb4; $use_utf8mb4,
$allow_myisam
) extends DBmysql {
public function __construct(
$dbhost,
$dbuser,
$dbpassword,
$dbdefault,
$log_deprecation_warnings,
$use_utf8mb4,
$allow_myisam
) {
$this->dbhost = $dbhost;
$this->dbuser = $dbuser;
$this->dbpassword = $dbpassword;
$this->dbdefault = $dbdefault;
$this->use_utf8mb4 = $use_utf8mb4;
$this->allow_myisam = $allow_myisam;
$this->log_deprecation_warnings = $log_deprecation_warnings; $this->log_deprecation_warnings = $log_deprecation_warnings;
......
...@@ -165,7 +165,7 @@ class InstallCommand extends AbstractConfigureCommand { ...@@ -165,7 +165,7 @@ class InstallCommand extends AbstractConfigureCommand {
} }
if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) { if (!$this->isDbAlreadyConfigured() || $input->getOption('reconfigure')) {
$result = $this->configureDatabase($input, $output, false, true); $result = $this->configureDatabase($input, $output, false, true, false);
if (self::ABORTED_BY_USER === $result) { if (self::ABORTED_BY_USER === $result) {
return 0; // Considered as success return 0; // Considered as success
......
...@@ -36,7 +36,8 @@ if (!defined('GLPI_ROOT')) { ...@@ -36,7 +36,8 @@ if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly"); die("Sorry. You can't access this file directly");
} }
use DB; use DBConnection;
use DBmysql;
use Glpi\Console\AbstractCommand; use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
...@@ -52,6 +53,13 @@ class MyIsamToInnoDbCommand extends AbstractCommand { ...@@ -52,6 +53,13 @@ class MyIsamToInnoDbCommand extends AbstractCommand {
*/ */
const ERROR_TABLE_MIGRATION_FAILED = 1; const ERROR_TABLE_MIGRATION_FAILED = 1;
/**
* Error code returned if DB configuration file cannot be updated.
*
* @var integer
*/
const ERROR_UNABLE_TO_UPDATE_CONFIG = 2;
protected function configure() { protected function configure() {
parent::configure(); parent::configure();
...@@ -74,51 +82,59 @@ class MyIsamToInnoDbCommand extends AbstractCommand { ...@@ -74,51 +82,59 @@ class MyIsamToInnoDbCommand extends AbstractCommand {
if (0 === $myisam_tables->count()) { if (0 === $myisam_tables->count()) {
$output->writeln('<info>' . __('No migration needed.') . '</info>'); $output->writeln('<info>' . __('No migration needed.') . '</info>');
return 0; } else {
} if (!$no_interaction) {
// Ask for confirmation (unless --no-interaction)
/** @var QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
return 0;
}
}
if (!$no_interaction) { foreach ($myisam_tables as $table) {
// Ask for confirmation (unless --no-interaction) $table_name = DBmysql::quoteName($table['TABLE_NAME']);
/** @var QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$run = $question_helper->ask(
$input,
$output,
new ConfirmationQuestion(__('Do you want to continue?') . ' [Yes/no]', true)
);
if (!$run) {
$output->writeln( $output->writeln(
'<comment>' . __('Migration aborted.') . '</comment>', '<comment>' . sprintf(__('Migrating table "%s"...'), $table_name) . '</comment>',
OutputInterface::VERBOSITY_VERBOSE OutputInterface::VERBOSITY_VERBOSE
); );
return 0; $result = $this->db->query(sprintf('ALTER TABLE %s ENGINE = InnoDB', $table_name));
if (false === $result) {
$message = sprintf(
__('Migration of table "%s" failed with message "(%s) %s".'),
$table_name,
$this->db->errno(),
$this->db->error()
);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_TABLE_MIGRATION_FAILED;
}
} }
} }
foreach ($myisam_tables as $table) { if (!DBConnection::updateConfigProperty(DBConnection::PROPERTY_ALLOW_MYISAM, false)) {
$table_name = DB::quoteName($table['TABLE_NAME']); throw new \Glpi\Console\Exception\EarlyExitException(
$output->writeln( '<error>' . __('Unable to update DB configuration file.') . '</error>',
'<comment>' . sprintf(__('Migrating table "%s"...'), $table_name) . '</comment>', self::ERROR_UNABLE_TO_UPDATE_CONFIG
OutputInterface::VERBOSITY_VERBOSE
); );
$result = $this->db->query(sprintf('ALTER TABLE %s ENGINE = InnoDB', $table_name));
if (false === $result) {
$message = sprintf(
__('Migration of table "%s" failed with message "(%s) %s".'),
$table_name,
$this->db->errno(),
$this->db->error()
);
$output->writeln(
'<error>' . $message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_TABLE_MIGRATION_FAILED;
}
} }
$output->writeln('<info>' . __('Migration done.') . '</info>'); if ($myisam_tables->count() > 0) {
$output->writeln('<info>' . __('Migration done.') . '</info>');
}
return 0; // Success return 0; // Success
} }
......
...@@ -52,6 +52,12 @@ class DBConnection extends CommonDBTM { ...@@ -52,6 +52,12 @@ class DBConnection extends CommonDBTM {
*/ */
public const PROPERTY_USE_UTF8MB4 = 'use_utf8mb4'; public const PROPERTY_USE_UTF8MB4 = 'use_utf8mb4';
/**
* "Allow MyISAM" property name.
* @var string
*/
public const PROPERTY_ALLOW_MYISAM = 'allow_myisam';
static protected $notable = true; static protected $notable = true;
...@@ -71,6 +77,7 @@ class DBConnection extends CommonDBTM { ...@@ -71,6 +77,7 @@ class DBConnection extends CommonDBTM {
* @param string $dbname The name of the DB * @param string $dbname The name of the DB
* @param boolean $log_deprecation_warnings Flag that indicates if DB deprecation warnings should be logged * @param boolean $log_deprecation_warnings Flag that indicates if DB deprecation warnings should be logged
* @param boolean $use_utf8mb4 Flag that indicates if utf8mb4 charset/collation should be used * @param boolean $use_utf8mb4 Flag that indicates if utf8mb4 charset/collation should be used
* @param boolean $allow_myisam Flag that indicates if MyISAM engine usage should be allowed
* @param string $config_dir * @param string $config_dir
* *
* @return boolean * @return boolean
...@@ -82,6 +89,7 @@ class DBConnection extends CommonDBTM { ...@@ -82,6 +89,7 @@ class DBConnection extends CommonDBTM {
string $dbname, string $dbname,
bool $log_deprecation_warnings = false, bool $log_deprecation_warnings = false,
bool $use_utf8mb4 = false, bool $use_utf8mb4 = false,
bool $allow_myisam = true,
string $config_dir = GLPI_CONFIG_DIR string $config_dir = GLPI_CONFIG_DIR
): bool { ): bool {
...@@ -97,6 +105,9 @@ class DBConnection extends CommonDBTM { ...@@ -97,6 +105,9 @@ class DBConnection extends CommonDBTM {
if ($use_utf8mb4) { if ($use_utf8mb4) {
$properties[self::PROPERTY_USE_UTF8MB4] = true; $properties[self::PROPERTY_USE_UTF8MB4] = true;
} }
if (!$allow_myisam) {
$properties[self::PROPERTY_ALLOW_MYISAM] = false;
}
$config_str = '<?php' . "\n" . 'class DB extends DBmysql {' . "\n"; $config_str = '<?php' . "\n" . 'class DB extends DBmysql {' . "\n";
foreach ($properties as $name => $value) { foreach ($properties as $name => $value) {
...@@ -193,6 +204,7 @@ class DBConnection extends CommonDBTM { ...@@ -193,6 +204,7 @@ class DBConnection extends CommonDBTM {
* @param string $dbname The name of the DB * @param string $dbname The name of the DB
* @param boolean $log_deprecation_warnings Flag that indicates if DB deprecation warnings should be logged * @param boolean $log_deprecation_warnings Flag that indicates if DB deprecation warnings should be logged
* @param boolean $use_utf8mb4 Flag that indicates if utf8mb4 charset/collation should be used * @param boolean $use_utf8mb4 Flag that indicates if utf8mb4 charset/collation should be used
* @param boolean $allow_myisam Flag that indicates if MyISAM engine usage should be allowed
* @param string $config_dir * @param string $config_dir
* *
* @return boolean for success * @return boolean for success
...@@ -204,6 +216,7 @@ class DBConnection extends CommonDBTM { ...@@ -204,6 +216,7 @@ class DBConnection extends CommonDBTM {
string $dbname, string $dbname,
bool $log_deprecation_warnings = false, bool $log_deprecation_warnings = false,
bool $use_utf8mb4 = false, bool $use_utf8mb4 = false,
bool $allow_myisam = true,
string $config_dir = GLPI_CONFIG_DIR string $config_dir = GLPI_CONFIG_DIR
): bool { ): bool {
...@@ -226,6 +239,9 @@ class DBConnection extends CommonDBTM { ...@@ -226,6 +239,9 @@ class DBConnection extends CommonDBTM {
if ($use_utf8mb4) { if ($use_utf8mb4) {
$properties[self::PROPERTY_USE_UTF8MB4] = true; $properties[self::PROPERTY_USE_UTF8MB4] = true;
} }
if (!$allow_myisam) {
$properties[self::PROPERTY_ALLOW_MYISAM] = false;
}
$config_str = '<?php' . "\n" . 'class DB extends DBmysql {' . "\n"; $config_str = '<?php' . "\n" . 'class DB extends DBmysql {' . "\n";
foreach ($properties as $name => $value) { foreach ($properties as $name => $value) {
...@@ -268,7 +284,15 @@ class DBConnection extends CommonDBTM { ...@@ -268,7 +284,15 @@ class DBConnection extends CommonDBTM {
**/ **/
static function createDBSlaveConfig() { static function createDBSlaveConfig() {
global $DB; global $DB;
self::createSlaveConnectionFile("localhost", "glpi", "glpi", "glpi", $DB->log_deprecation_warnings, $DB->use_utf8mb4); self::createSlaveConnectionFile(
"localhost",
"glpi",
"glpi",
"glpi",
$DB->log_deprecation_warnings,
$DB->use_utf8mb4,
$DB->allow_myisam
);
} }
...@@ -282,7 +306,15 @@ class DBConnection extends CommonDBTM { ...@@ -282,7 +306,15 @@ class DBConnection extends CommonDBTM {
**/ **/
static function saveDBSlaveConf($host, $user, $password, $DBname) { static function saveDBSlaveConf($host, $user, $password, $DBname) {
global $DB; global $DB;
self::createSlaveConnectionFile($host, $user, $password, $DBname, $DB->log_deprecation_warnings, $DB->use_utf8mb4); self::createSlaveConnectionFile(
$host,
$user,
$password,
$DBname,
$DB->log_deprecation_warnings,
$DB->use_utf8mb4,
$DB->allow_myisam
);
} }
......
...@@ -127,6 +127,14 @@ class DBmysql { ...@@ -127,6 +127,14 @@ class DBmysql {
*/ */
public $use_utf8mb4 = false; public $use_utf8mb4 = false;
/**
* Determine if MyISAM engine usage should be allowed for tables creation/altering operations.
* Defaults to true to keep backward compatibility with old DB.
*
* @var bool
*/
public $allow_myisam = true;
/** Is it a first connection ? /** Is it a first connection ?
* Indicates if the first connection attempt is successful or not * Indicates if the first connection attempt is successful or not
...@@ -311,28 +319,7 @@ class DBmysql { ...@@ -311,28 +319,7 @@ class DBmysql {
$TIMER->start(); $TIMER->start();
} }
if (preg_match('/(ALTER|CREATE)\s+TABLE\s+/', $query)) { $this->checkForDeprecatedTableOptions($query);
$matches = [];
if ($this->use_utf8mb4 && preg_match('/(?<invalid>(utf8(_[^\';\s]+)?))[\';\s]/', $query, $matches)) {
trigger_error(
sprintf(
'Usage of "%s" charset/collation detected, should be "%s"',
$matches['invalid'],
str_replace('utf8', 'utf8mb4', $matches['invalid'])
),
E_USER_WARNING
);
} else if (!$this->use_utf8mb4 && preg_match('/(?<invalid>(utf8mb4(_[^\';\s]+)?))[\';\s]/', $query, $matches)) {
trigger_error(
sprintf(
'Usage of "%s" charset/collation detected, should be "%s"',
$matches['invalid'],
str_replace('utf8mb4', 'utf8', $matches['invalid'])
),
E_USER_WARNING
);
}
}
$res = $this->dbh->query($query); $res = $this->dbh->query($query);
if (!$res) { if (!$res) {
...@@ -589,10 +576,20 @@ class DBmysql { ...@@ -589,10 +576,20 @@ class DBmysql {
/** /**
* Returns tables using "MyIsam" engine. * Returns tables using "MyIsam" engine.
* *
* @param bool $exclude_plugins
*
* @return DBmysqlIterator * @return DBmysqlIterator
*/ */
public function getMyIsamTables(): DBmysqlIterator { public function getMyIsamTables(bool $exclude_plugins = false): DBmysqlIterator {
$iterator = $this->listTables('glpi\_%', ['engine' => 'MyIsam']); $criteria = [
'engine' => 'MyIsam',
];
if ($exclude_plugins) {
$criteria[] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]];
}
$iterator = $this->listTables('glpi\_%', $criteria);
return $iterator; return $iterator;
} }
...@@ -1755,6 +1752,47 @@ class DBmysql { ...@@ -1755,6 +1752,47 @@ class DBmysql {
} }
} }
/**
* Check for deprecated table options during ALTER/CREATE TABLE queries.
*
* @param string $query
*
* @return void
*/
private function checkForDeprecatedTableOptions(string $query): void {
if (preg_match('/(ALTER|CREATE)\s+TABLE\s+/', $query) !== 1) {
return;
}
// Wrong UTF8 charset/collation
$matches = [];
if ($this->use_utf8mb4 && preg_match('/(?<invalid>(utf8(_[^\';\s]+)?))([\';\s]|$)/', $query, $matches)) {
trigger_error(
sprintf(
'Usage of "%s" charset/collation detected, should be "%s"',
$matches['invalid'],
str_replace('utf8', 'utf8mb4', $matches['invalid'])
),
E_USER_WARNING
);
} else if (!$this->use_utf8mb4 && preg_match('/(?<invalid>(utf8mb4(_[^\';\s]+)?))([\';\s]|$)/', $query, $matches)) {
trigger_error(
sprintf(
'Usage of "%s" charset/collation detected, should be "%s"',
$matches['invalid'],
str_replace('utf8mb4', 'utf8', $matches['invalid'])
),
E_USER_WARNING
);
}
// Usage of MyISAM
$matches = [];
if (!$this->allow_myisam && preg_match('/[)\s]engine\s*=\s*\'?myisam([\';\s]|$)/i', $query, $matches)) {
trigger_error('Usage of "MyISAM" engine is discouraged, please use "InnoDB" engine.', E_USER_WARNING);
}
}
/** /**
* Return configuration boolean properties computed using current state of tables. * Return configuration boolean properties computed using current state of tables.
* *
...@@ -1768,6 +1806,11 @@ class DBmysql { ...@@ -1768,6 +1806,11 @@ class DBmysql {
$config_flags[DBConnection::PROPERTY_USE_UTF8MB4] = true; $config_flags[DBConnection::PROPERTY_USE_UTF8MB4] = true;
} }
if ($this->getMyIsamTables(true)->count() === 0) {
// Disallow MyISAM if there is no core table still using this engine.
$config_flags[DBConnection::PROPERTY_ALLOW_MYISAM] = false;
}
return $config_flags; return $config_flags;
} }
} }
...@@ -264,7 +264,16 @@ function step4 ($databasename, $newdatabasename) { ...@@ -264,7 +264,16 @@ function step4 ($databasename, $newdatabasename) {
prev_form($host, $user, $password); prev_form($host, $user, $password);
} else { } else {
if (DBConnection::createMainConfig($host, $user, $password, $databasename, false, true)) { $success = DBConnection::createMainConfig(
$host,
$user,
$password,
$databasename,
false,
true,
false
);
if ($success) {
Toolbox::createSchema($_SESSION["glpilanguage"]); Toolbox::createSchema($_SESSION["glpilanguage"]);
echo "<p>".__('OK - database was initialized')."</p>"; echo "<p>".__('OK - database was initialized')."</p>";
...@@ -281,7 +290,16 @@ function step4 ($databasename, $newdatabasename) { ...@@ -281,7 +290,16 @@ function step4 ($databasename, $newdatabasename) {
if ($link->select_db($newdatabasename)) { if ($link->select_db($newdatabasename)) {
echo "<p>".__('Database created')."</p>"; echo "<p>".__('Database created')."</p>";
if (DBConnection::createMainConfig($host, $user, $password, $newdatabasename, false, true)) { $success = DBConnection::createMainConfig(
$host,
$user,
$password,
$newdatabasename,
false,
true,
false
);
if ($success) {
Toolbox::createSchema($_SESSION["glpilanguage"]); Toolbox::createSchema($_SESSION["glpilanguage"]);
echo "<p>".__('OK - database was initialized')."</p>"; echo "<p>".__('OK - database was initialized')."</p>";
next_form(); next_form();
...@@ -295,9 +313,21 @@ function step4 ($databasename, $newdatabasename) { ...@@ -295,9 +313,21 @@ function step4 ($databasename, $newdatabasename) {
if ($link->query("CREATE DATABASE IF NOT EXISTS `".$newdatabasename."`")) { if ($link->query("CREATE DATABASE IF NOT EXISTS `".$newdatabasename."`")) {
echo "<p>".__('Database created')."</p>"; echo "<p>".__('Database created')."</p>";
if ($link->select_db($newdatabasename) $select_db = $link->select_db($newdatabasename);
&& DBConnection::createMainConfig($host, $user, $password, $newdatabasename, false, true)) { $success = false;
if ($select_db) {