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

Store timezone usage state in config file; add a command to enable it

parent 6682ec9a
......@@ -127,6 +127,7 @@ The present file will list all changes made to the project; according to the
- `Config::displayCheckExtensions()`
- `Config::getCache()`
- `DBMysql::affected_rows()`
- `DBMysql::areTimezonesAvailable()`
- `DBMysql::data_seek()`
- `DBMysql::fetch_array()`
- `DBMysql::fetch_assoc()`
......@@ -138,6 +139,7 @@ The present file will list all changes made to the project; according to the
- `DBMysql::insert_id()`
- `DBMysql::isMySQLStrictMode()`
- `DBMysql::list_fields()`
- `DBMysql::notTzMigrated()`
- `DBMysql::num_fields()`
- `DbUtils::getRealQueryForTreeItem()`
- `Dropdown::getDropdownNetpoint()`
......
......@@ -479,14 +479,11 @@ class Central extends CommonGLPI {
count($myisam_tables)
);
}
if ($DB->areTimezonesAvailable()) {
$not_tstamp = $DB->notTzMigrated();
if ($not_tstamp > 0) {
if ($DB->use_timezones && ($not_tstamp = $DB->getTzIncompatibleTables()->count()) > 0) {
$messages['warnings'][] = sprintf(__('%1$s columns are not compatible with timezones usage.'), $not_tstamp)
. ' '
. sprintf(__('Run the "php bin/console %1$s" command to migrate them.'), 'glpi:migration:timestamps');
}
}
if (($non_utf8mb4_tables = $DB->getNonUtf8mb4Tables()->count()) > 0) {
$messages['warnings'][] = sprintf(__('%1$s tables not migrated to utf8mb4 collation.'), $non_utf8mb4_tables)
. ' '
......
......@@ -1297,9 +1297,7 @@ class Config extends CommonDBTM {
echo "</td>";
echo "<td><label for='dropdown_timezone$rand'>" . __('Timezone') . "</label></td>";
echo "<td>";
$tz_warning = '';
$tz_available = $DB->areTimezonesAvailable($tz_warning);
if ($tz_available) {
if ($DB->use_timezones) {
$timezones = $DB->getTimezones();
Dropdown::showFromArray(
'timezone',
......@@ -1310,8 +1308,9 @@ class Config extends CommonDBTM {
]
);
} else {
echo "<img src=\"{$CFG_GLPI['root_doc']}/pics/warning_min.png\">";
echo $tz_warning;
echo __('Timezone usage has not been activated.')
. ' '
. sprintf(__('Run the "php bin/console %1$s" command to activate it.'), 'glpi:database:enable_timezones');
}
echo "<tr class='tab_bg_2'><td><label for='dropdown_default_central_tab$rand'>".__('Default central tab')."</label></td>";
......
......@@ -41,6 +41,7 @@ use DBConnection;
use DBmysql;
use Glpi\Console\AbstractCommand;
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
use Glpi\System\Requirement\DbTimezones;
use mysqli;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Table;
......@@ -282,6 +283,8 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
DBConnection::setConnectionCharset($mysqli, $use_utf8mb4);
$use_timezones = $this->checkTimezonesAvailability($mysqli);
$db_name = $mysqli->real_escape_string($db_name);
$output->writeln(
......@@ -293,6 +296,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
$db_user,
$db_pass,
$db_name,
$use_timezones,
$log_deprecation_warnings,
$use_utf8mb4,
$allow_myisam
......@@ -315,6 +319,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
$db_user,
$db_pass,
$db_name,
$use_timezones,
$log_deprecation_warnings,
$use_utf8mb4,
$allow_myisam
......@@ -324,6 +329,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
$dbuser,
$dbpassword,
$dbdefault,
$use_timezones,
$log_deprecation_warnings,
$use_utf8mb4,
$allow_myisam
......@@ -332,6 +338,7 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
$this->dbuser = $dbuser;
$this->dbpassword = $dbpassword;
$this->dbdefault = $dbdefault;
$this->use_timezones = $use_timezones;
$this->use_utf8mb4 = $use_utf8mb4;
$this->allow_myisam = $allow_myisam;
......@@ -431,4 +438,55 @@ abstract class AbstractConfigureCommand extends AbstractCommand implements Force
new ConfirmationQuestion(__('Do you want to continue?') . ' [Yes/no]', true)
);
}
/**
* Check timezones availability and return availability state.
*
* @param mysqli $mysqli
*
* @return bool
*/
private function checkTimezonesAvailability(mysqli $mysqli): bool {
$db = new class($mysqli) extends DBmysql {
public function __construct($dbh) {
$this->dbh = $dbh;
}
};
$timezones_requirement = new DbTimezones($db);
if (!$timezones_requirement->isValidated()) {
$message = __('Timezones usage cannot be activated due to following errors:');
foreach ($timezones_requirement->getValidationMessages() as $validation_message) {
$message .= "\n - " . $validation_message;
}
$this->output->writeln(
'<comment>' . $message . '</comment>',
OutputInterface::VERBOSITY_QUIET
);
if ($this->input->getOption('no-interaction')) {
$message = sprintf(
__('Fix them and run the "php bin/console %1$s" command to enable timezones.'),
'glpi:database:enable_timezones'
);
$this->output->writeln('<comment>' . $message . '</comment>', OutputInterface::VERBOSITY_QUIET);
} else {
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
$question_helper = $this->getHelper('question');
$continue = $question_helper->ask(
$this->input,
$this->output,
new ConfirmationQuestion(__('Do you want to continue?') . ' [Yes/no]', true)
);
if (!$continue) {
throw new \Glpi\Console\Exception\EarlyExitException(
'<comment>' . __('Configuration aborted.') . '</comment>',
self::ABORTED_BY_USER
);
}
}
}
return $timezones_requirement->isValidated();
}
}
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2021 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Console\Database;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DBConnection;
use Glpi\Console\AbstractCommand;
use Glpi\System\Requirement\DbTimezones;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class EnableTimezonesCommand extends AbstractCommand {
/**
* Error code returned if DB configuration file cannot be updated.
*
* @var integer
*/
const ERROR_UNABLE_TO_UPDATE_CONFIG = 1;
/**
* Error code returned if prerequisites are missing.
*
* @var integer
*/
const ERROR_MISSING_PREREQUISITES = 2;
/**
* Error code returned if some tables are still using datetime field type.
*
* @var integer
*/
const ERROR_TIMESTAMP_FIELDS_REQUIRED = 3;
protected function configure() {
parent::configure();
$this->setName('glpi:database:enable_timezones');
$this->setAliases(['db:enable_timezones']);
$this->setDescription(__('Enable timezones usage.'));
}
protected function execute(InputInterface $input, OutputInterface $output) {
$timezones_requirement = new DbTimezones($this->db);
if (!$timezones_requirement->isValidated()) {
$message = __('Timezones usage cannot be activated due to following errors:');
foreach ($timezones_requirement->getValidationMessages() as $validation_message) {
$message .= "\n - " . $validation_message;
}
throw new \Glpi\Console\Exception\EarlyExitException(
'<error>' . $message . '</error>',
self::ERROR_MISSING_PREREQUISITES
);
}
if (($datetime_count = $this->db->getTzIncompatibleTables()->count()) > 0) {
$message = sprintf(__('%1$s columns are not still using datetime field type.'), $datetime_count)
. ' '
. sprintf(__('Run the "php bin/console %1$s" command to migrate them.'), 'glpi:migration:timestamps');
throw new \Glpi\Console\Exception\EarlyExitException(
'<error>' . $message . '</error>',
self::ERROR_TIMESTAMP_FIELDS_REQUIRED
);
}
if (!DBConnection::updateConfigProperty(DBConnection::PROPERTY_USE_TIMEZONES, true)) {
throw new \Glpi\Console\Exception\EarlyExitException(
'<error>' . __('Unable to update DB configuration file.') . '</error>',
self::ERROR_UNABLE_TO_UPDATE_CONFIG
);
}
$output->writeln('<info>' . __('Timezone usage has been enabled.') . '</info>');
return 0; // Success
}
}
......@@ -36,8 +36,9 @@ if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use DBConnection;
use Glpi\Console\AbstractCommand;
use QueryExpression;
use Glpi\System\Requirement\DbTimezones;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
......@@ -45,6 +46,20 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
class TimestampsCommand extends AbstractCommand {
/**
* Error code returned when failed to migrate one table.
*
* @var integer
*/
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() {
parent::configure();
......@@ -56,36 +71,7 @@ class TimestampsCommand extends AbstractCommand {
//convert db
// we are going to update datetime types to timestamp type
$tbl_iterator = $this->db->request([
'SELECT' => ['information_schema.columns.table_name as TABLE_NAME'],
'DISTINCT' => true,
'FROM' => 'information_schema.columns',
'INNER JOIN' => [
'information_schema.tables' => [
'FKEY' => [
'information_schema.tables' => 'table_name',
'information_schema.columns' => 'table_name',
[
'AND' => [
'information_schema.tables.table_schema' => new QueryExpression(
$this->db->quoteName('information_schema.columns.table_schema')
),
]
],
]
]
],
'WHERE' => [
'information_schema.columns.table_schema' => $this->db->dbdefault,
'information_schema.columns.table_name' => ['LIKE', 'glpi\_%'],
'information_schema.columns.data_type' => 'datetime',
'information_schema.tables.table_type' => 'BASE TABLE',
],
'ORDER' => [
'information_schema.columns.table_name'
]
]);
$tbl_iterator = $this->db->getTzIncompatibleTables();
$output->writeln(
sprintf(
......@@ -96,9 +82,7 @@ class TimestampsCommand extends AbstractCommand {
if ($tbl_iterator->count() === 0) {
$output->writeln('<info>' . __('No migration needed.') . '</info>');
return 0; // Success
}
} else {
if (!$input->getOption('no-interaction')) {
// Ask for confirmation (unless --no-interaction)
/** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
......@@ -213,13 +197,43 @@ class TimestampsCommand extends AbstractCommand {
$progress_bar,
OutputInterface::VERBOSITY_QUIET
);
return self::ERROR_TABLE_MIGRATION_FAILED;
}
}
$progress_bar->finish();
$this->output->write(PHP_EOL);
}
$timezones_requirement = new DbTimezones($this->db);
if ($timezones_requirement->isValidated()) {
if (!DBConnection::updateConfigProperty(DBConnection::PROPERTY_USE_TIMEZONES, true)) {
throw new \Glpi\Console\Exception\EarlyExitException(
'<error>' . __('Unable to update DB configuration file.') . '</error>',
self::ERROR_UNABLE_TO_UPDATE_CONFIG
);
}
} else {
$output->writeln(
'<error>' . __('Timezones usage cannot be activated due to following errors:') . '</error>',
OutputInterface::VERBOSITY_QUIET
);
foreach ($timezones_requirement->getValidationMessages() as $validation_message) {
$output->writeln(
'<error> - ' . $validation_message . '</error>',
OutputInterface::VERBOSITY_QUIET
);
}
$message = sprintf(
__('Fix them and run the "php bin/console %1$s" command to enable timezones.'),
'glpi:database:enable_timezones'
);
$output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
}
if ($tbl_iterator->count() > 0) {
$output->writeln('<info>' . __('Migration done.') . '</info>');
}
return 0; // Success
}
......
......@@ -40,6 +40,12 @@ if (!defined('GLPI_ROOT')) {
**/
class DBConnection extends CommonDBTM {
/**
* "Use timezones" property name.
* @var string
*/
public const PROPERTY_USE_TIMEZONES = 'use_timezones';
/**
* "Log deprecation warnings" property name.
* @var string
......@@ -75,6 +81,7 @@ class DBConnection extends CommonDBTM {
* @param string $user The DB user
* @param string $password The DB password
* @param string $dbname The name of the DB
* @param boolean $use_timezones Flag that indicates if timezones usage should be activated
* @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 $allow_myisam Flag that indicates if MyISAM engine usage should be allowed
......@@ -87,6 +94,7 @@ class DBConnection extends CommonDBTM {
string $user,
string $password,
string $dbname,
bool $use_timezones = false,
bool $log_deprecation_warnings = false,
bool $use_utf8mb4 = false,
bool $allow_myisam = true,
......@@ -99,6 +107,9 @@ class DBConnection extends CommonDBTM {
'dbpassword' => rawurlencode($password),
'dbdefault' => $dbname,
];
if ($use_timezones) {
$properties[self::PROPERTY_USE_TIMEZONES] = true;
}
if ($log_deprecation_warnings) {
$properties[self::PROPERTY_LOG_DEPRECATION_WARNINGS] = true;
}
......@@ -202,6 +213,7 @@ class DBConnection extends CommonDBTM {
* @param string $user The DB user
* @param string $password The DB password
* @param string $dbname The name of the DB
* @param boolean $use_timezones Flag that indicates if timezones usage should be activated
* @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 $allow_myisam Flag that indicates if MyISAM engine usage should be allowed
......@@ -214,6 +226,7 @@ class DBConnection extends CommonDBTM {
string $user,
string $password,
string $dbname,
bool $use_timezones = false,
bool $log_deprecation_warnings = false,
bool $use_utf8mb4 = false,
bool $allow_myisam = true,
......@@ -233,6 +246,9 @@ class DBConnection extends CommonDBTM {
'dbpassword' => rawurlencode($password),
'dbdefault' => $dbname,
];
if ($use_timezones) {
$properties[self::PROPERTY_USE_TIMEZONES] = true;
}
if ($log_deprecation_warnings) {
$properties[self::PROPERTY_LOG_DEPRECATION_WARNINGS] = true;
}
......@@ -289,6 +305,7 @@ class DBConnection extends CommonDBTM {
"glpi",
"glpi",
"glpi",
$DB->use_timezones,
$DB->log_deprecation_warnings,
$DB->use_utf8mb4,
$DB->allow_myisam
......@@ -311,6 +328,7 @@ class DBConnection extends CommonDBTM {
$user,
$password,
$DBname,
$DB->use_timezones,
$DB->log_deprecation_warnings,
$DB->use_utf8mb4,
$DB->allow_myisam
......
......@@ -35,6 +35,7 @@ if (!defined('GLPI_ROOT')) {
}
use Glpi\Application\ErrorHandler;
use Glpi\System\Requirement\DbTimezones;
/**
* Database class for Mysql
......@@ -111,6 +112,14 @@ class DBmysql {
*/
public $dbsslcacipher = null;
/**
* Determine if timezones should be used for timestamp fields.
* Defaults to false to keep backward compatibility with old DB.
*
* @var bool
*/
public $use_timezones = false;
/**
* Determine if warnings related to MySQL deprecations should be logged too.
* Defaults to false as this option should only on development/test environment.
......@@ -255,6 +264,7 @@ class DBmysql {
* @since 9.5.0
*/
protected function guessTimezone() {
if ($this->use_timezones) {
if (isset($_SESSION['glpi_tz'])) {
$zone = $_SESSION['glpi_tz'];
} else {
......@@ -272,6 +282,9 @@ class DBmysql {
}
$zone = !empty($conf_tz['value']) ? $conf_tz['value'] : date_default_timezone_get();
}
} else {
$zone = date_default_timezone_get();
}
return $zone;
}
......@@ -659,6 +672,54 @@ class DBmysql {
return $iterator;
}
/**
* Returns tables not compatible with timezone usage, i.e. having "datetime" columns.
*
* @param bool $exclude_plugins
*
* @return DBmysqlIterator
*
* @since 10.0.0
*/
public function getTzIncompatibleTables(bool $exclude_plugins = false): DBmysqlIterator {
$query = [
'SELECT' => ['information_schema.columns.table_name as TABLE_NAME'],
'DISTINCT' => true,
'FROM' => 'information_schema.columns',
'INNER JOIN' => [
'information_schema.tables' => [
'FKEY' => [
'information_schema.tables' => 'table_name',
'information_schema.columns' => 'table_name',
[
'AND' => [
'information_schema.tables.table_schema' => new QueryExpression(
$this->quoteName('information_schema.columns.table_schema')
),
]
],
]
]
],
'WHERE' => [
'information_schema.tables.table_schema' => $this->dbdefault,
'information_schema.tables.table_name' => ['LIKE', 'glpi\_%'],
'information_schema.tables.table_type' => 'BASE TABLE',
'information_schema.columns.data_type' => 'datetime',
],
'ORDER' => ['TABLE_NAME']
];
if ($exclude_plugins) {
$query['WHERE'][] = ['NOT' => ['information_schema.tables.table_name' => ['LIKE', 'glpi\_plugin\_%']]];
}
$iterator = $this->request($query);
return $iterator;
}
/**
* List fields of a table
*
......@@ -1459,55 +1520,6 @@ class DBmysql {
return $this->in_transaction;
}
/**
* Check if timezone data is accessible and available in database.
*
* @param string $msg Variable that would contain the reason of data unavailability.
*
* @return boolean
*
* @since 9.5.0
*/
public function areTimezonesAvailable(string &$msg = '') {
global $GLPI_CACHE;
if ($GLPI_CACHE->has('are_timezones_available')) {
return $GLPI_CACHE->get('are_timezones_available');
}
$GLPI_CACHE->set('are_timezones_available', false, DAY_TIMESTAMP);
$mysql_db_res = $this->request('SHOW DATABASES LIKE ' . $this->quoteValue('mysql'));
if ($mysql_db_res->count() === 0) {
$msg = __('Access to timezone database (mysql) is not allowed.');
return false;
}
$tz_table_res = $this->request(
'SHOW TABLES FROM '
. $this->quoteName('mysql')
. ' LIKE '
. $this->quoteValue('time_zone_name')
);
if ($tz_table_res->count() === 0) {
$msg = __('Access to timezone table (mysql.time_zone_name) is not allowed.');
return false;
}
$criteria = [
'COUNT' => 'cpt',
'FROM' => 'mysql.time_zone_name',
];
$iterator = $this->request($criteria);
$result = $iterator->current();
if ($result['cpt'] == 0) {
$msg = __('Timezones seems not loaded, see https://glpi-install.readthedocs.io/en/latest/timezones.html.');
return false;
}
$GLPI_CACHE->set('are_timezones_available', true);
return true;
}
/**
* Defines timezone to use.