diff --git a/application/utils.inc.php b/application/utils.inc.php index 1e91239cc..ed5adb49e 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -874,11 +874,50 @@ class utils */ public static function DateTimeFormatToPHP($sOldDateTimeFormat) { - $aSearch = array('%d', '%m', '%y', '%Y', '%H', '%i', '%s'); - $aReplacement = array('d', 'm', 'y', 'Y', 'H', 'i', 's'); + $aSearch = ['%d', '%m', '%y', '%Y', '%H', '%i', '%s']; + $aReplacement = ['d', 'm', 'y', 'Y', 'H', 'i', 's']; return str_replace($aSearch, $aReplacement, $sOldDateTimeFormat); } + /** + * Convert an old strtime() date/time format specification {@link https://www.php.net/manual/fr/function.strftime.php} + * to a format compatible with \DateTime::format {@link https://www.php.net/manual/fr/datetime.format.php} + * + * Example: '%Y-%m-%d %H:%M:%S' => 'Y-m-d H:i:s' + * + * Note: Not all strftime() formats can be converted, in which case they will be present in the returned string (eg. '%U' or '%W') + * + * @param string $sOldStrftimeFormat + * + * @return string + * @since 3.1.0 + */ + public static function StrftimeFormatToDateTimeFormat(string $sOldStrftimeFormat): string + { + $aSearch = [ + '%d', '%m', '%y', '%Y', '%H', '%M', '%S', // Most popular formats + '%a', '%A', '%e', '%j', '%u', '%w', // Day formats + '%U', '%V', '%W', // Week formats + '%b', '%B', '%h', // Month formats + '%C', '%g', '%G', // Year formats + '%k', '%I', '%l', '%p', '%P', '%r', '%R', '%T', '%X', '%z', '%Z', // Time formats + '%c', '%D', '%F', '%s', '%x', // Datetime formats + '%n', '%t', '%%', // Misc. formats + ]; + $aReplacement = [ + 'd', 'm', 'y', 'Y', 'H', 'i', 's', + 'D', 'l', 'j', 'z', 'N', 'w', + '%U', 'W', '%W', + 'M', 'F', 'M', + '%C', 'y', 'Y', + 'G', 'h', 'g', 'A', 'a', 'h:i:s A', 'H:i', 'H:i:s', '%X', 'O', 'T', + '%c', 'm/d/y', 'Y-m-d', 'U', '%x', + '%n', '%t', '%', + ]; + + return str_replace($aSearch, $aReplacement, $sOldStrftimeFormat); + } + /** * Allow to set cached config. Useful when running with {@link Parameters} for example. * @param \Config $oConfig diff --git a/core/log.class.inc.php b/core/log.class.inc.php index 7314202f6..b8a2d358e 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -551,6 +551,12 @@ class LogChannels */ public const NOTIFICATIONS = 'notifications'; + /** + * @var string Everything related to the backup / restore + * @since 3.1.0 + */ + public const BACKUP = 'backup'; + /** * @since 3.0.0 */ diff --git a/datamodels/2.x/itop-backup/backup.params.distrib b/datamodels/2.x/itop-backup/backup.params.distrib index 8c9bfd304..98b02de02 100644 --- a/datamodels/2.x/itop-backup/backup.params.distrib +++ b/datamodels/2.x/itop-backup/backup.params.distrib @@ -23,7 +23,7 @@ auth_pwd = admin # Full path or relative to current directory # # Formatting rules: -# %Y-%m-%d => 2011-01-25... see PHP documentation of strftime() +# %Y-%m-%d => 2011-01-25... see PHP documentation of strftime() (https://www.php.net/manual/fr/function.strftime.php) # Placeholders: # __HOST__ MySQL server # __DB__ Database name diff --git a/datamodels/2.x/itop-backup/backup.php b/datamodels/2.x/itop-backup/backup.php index 3c8f45fad..eb4453fa8 100644 --- a/datamodels/2.x/itop-backup/backup.php +++ b/datamodels/2.x/itop-backup/backup.php @@ -69,7 +69,7 @@ function Usage($oP) $oP->p('auth_user: login, must be administrator'); $oP->p('auth_pwd: ...'); } - $oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime format spec. The following placeholders are available: __HOST__, __DB__, __SUBNAME__'); + $oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime() format spec (https://www.php.net/manual/fr/function.strftime.php). The following placeholders are available: __HOST__, __DB__, __SUBNAME__'); $oP->p('simulate [optional]: set to check the name of the file that would be created'); $oP->p('mysql_bindir [optional]: specify the path for mysqldump'); diff --git a/datamodels/2.x/itop-backup/check-backup.php b/datamodels/2.x/itop-backup/check-backup.php index 01b171cec..e8ec371a7 100644 --- a/datamodels/2.x/itop-backup/check-backup.php +++ b/datamodels/2.x/itop-backup/check-backup.php @@ -108,20 +108,9 @@ function MakeArchiveFileName($iRefTime = null) $sDefaultBackupFileName = sys_get_temp_dir().'/'."__DB__-%Y-%m-%d"; $sBackupFile = utils::ReadParam('backup_file', $sDefaultBackupFileName, true, 'raw_data'); - $oConfig = GetConfig(); - - $sBackupFile = str_replace('__HOST__', $oConfig->Get('db_host'), $sBackupFile); - $sBackupFile = str_replace('__DB__', $oConfig->Get('db_name'), $sBackupFile); - $sBackupFile = str_replace('__SUBNAME__', $oConfig->Get('db_subname'), $sBackupFile); - - if (is_null($iRefTime)) - { - $sBackupFile = strftime($sBackupFile); - } - else - { - $sBackupFile = strftime($sBackupFile, $iRefTime); - } + $oBackup = new DBBackup(); + $oDateTime = $iRefTime !== null ? new DateTime($iRefTime) : new DateTime(); + $sBackupFile = $oBackup->MakeName($sBackupFile, $oDateTime); return $sBackupFile; } diff --git a/datamodels/2.x/itop-backup/main.itop-backup.php b/datamodels/2.x/itop-backup/main.itop-backup.php index 877025c03..e45037bfe 100644 --- a/datamodels/2.x/itop-backup/main.itop-backup.php +++ b/datamodels/2.x/itop-backup/main.itop-backup.php @@ -163,8 +163,7 @@ class BackupExec extends AbstractWeeklyScheduledProcess // $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting($this->GetModuleName(), 'mysql_bindir', '')); - $sBackupFileFormat = MetaModel::GetConfig()->GetModuleSetting($this->GetModuleName(), 'file_name_format', - '__DB__-%Y-%m-%d_%H_%M'); + $sBackupFileFormat = MetaModel::GetConfig()->GetModuleSetting($this->GetModuleName(), 'file_name_format', '__DB__-%Y-%m-%d_%H_%M'); $sName = $oBackup->MakeName($sBackupFileFormat); if ($sName == '') { diff --git a/datamodels/2.x/itop-backup/restore.php b/datamodels/2.x/itop-backup/restore.php index 2f0ff6e9f..00a50d998 100644 --- a/datamodels/2.x/itop-backup/restore.php +++ b/datamodels/2.x/itop-backup/restore.php @@ -69,7 +69,7 @@ function Usage($oP) $oP->p('auth_user: login, must be administrator'); $oP->p('auth_pwd: ...'); } - $oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime format spec. The following placeholders are available: __HOST__, __DB__, __SUBNAME__'); + $oP->p('backup_file [optional]: name of the file to store the backup into. Follows the PHP strftime() (https://www.php.net/manual/fr/function.strftime.php) format spec. The following placeholders are available: __HOST__, __DB__, __SUBNAME__'); $oP->p('mysql_bindir [optional]: specify the path for mysql executable'); if (utils::IsModeCLI()) @@ -131,7 +131,7 @@ function ExecuteMainOperation($oP){ exit; } -// Interpret strftime specifications (like %Y) and database placeholders + // Interpret strftime() specifications (like %Y) and database placeholders $oRestore = new MyCliRestore($oP); $oRestore->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); diff --git a/setup/backup.class.inc.php b/setup/backup.class.inc.php index e0448b9ff..2d0fb0461 100644 --- a/setup/backup.class.inc.php +++ b/setup/backup.class.inc.php @@ -146,18 +146,35 @@ class DBBackup /** * Create a normalized backup name, depending on the current date/time and Database * - * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting specs + * @param string sNameSpec Name and path, eventually containing itop placeholders + time formatting following the strftime() format {@link https://www.php.net/manual/fr/function.strftime.php} + * @param \DateTime|null $oDateTime Date time to use for the name * * @return string + * @since 3.1.0 N°5279 Add $oDtaeaTime parameter */ - public function MakeName($sNameSpec = "__DB__-%Y-%m-%d") + public function MakeName($sNameSpec = "__DB__-%Y-%m-%d", DateTime $oDateTime = null) { + if ($oDateTime === null) { + $oDateTime = new DateTime(); + } + $sFileName = $sNameSpec; $sFileName = str_replace('__HOST__', $this->sDBHost, $sFileName); $sFileName = str_replace('__DB__', $this->sDBName, $sFileName); $sFileName = str_replace('__SUBNAME__', $this->sDBSubName, $sFileName); - // Transform %Y, etc. - $sFileName = strftime($sFileName); + + // Transform date/time placeholders (%Y, %m, etc) + // N°5279 - As of PHP 8.1 strftime() is deprecated so we use \DateTime::format() instead + // + // IMPORTANT: We can't use \DateTime::format() directly on the whole filename as it would also format characters that are not supposed to be. eg. "__DB__-Y-m-d-production" would become "itopdb-2023-02-09-+01:00Thu, 09 Feb 2023 11:34:01 +0100202309" + $sFileName = preg_replace_callback( + '/(%[a-zA-Z])/', + function ($aMatches) use ($oDateTime) { + $sDateTimeFormatPlaceholder = utils::StrftimeFormatToDateTimeFormat($aMatches[0]); + return $oDateTime->format($sDateTimeFormatPlaceholder); + }, + $sFileName, + ); return $sFileName; } diff --git a/tests/php-unit-tests/unitary-tests/application/utilsTest.php b/tests/php-unit-tests/unitary-tests/application/utilsTest.php index 2dcc902b3..0e505fa4c 100644 --- a/tests/php-unit-tests/unitary-tests/application/utilsTest.php +++ b/tests/php-unit-tests/unitary-tests/application/utilsTest.php @@ -423,6 +423,35 @@ class utilsTest extends ItopTestCase ]; } + /** + * @dataProvider StrftimeFormatToDateTimeFormatProvider + * @covers \utils::StrftimeFormatToDateTimeFormat + * + * @param string $sInput + * @param string $sExpectedFormat + * + * @return void + */ + public function testStrftimeFormatToDateTimeFormat(string $sInput, string $sExpectedFormat) + { + $sTestedFormat = utils::StrftimeFormatToDateTimeFormat($sInput); + $this->assertEquals($sExpectedFormat, $sTestedFormat, "DateTime format transformation for '$sInput' doesn't match. Got '$sTestedFormat', expected '$sExpectedFormat'."); + } + + public function StrftimeFormatToDateTimeFormatProvider(): array + { + return [ + 'Standard date time' => [ + '%Y-%m-%d %H:%M:%S', + 'Y-m-d H:i:s', + ], + 'All placeholders' => [ + '%d | %m | %y | %Y | %H | %M | %S | %a | %A | %e | %j | %u | %w | %U | %V | %W | %b | %B | %h | %C | %g | %G | %k | %I | %l | %p | %P | %r | %R | %T | %X | %z | %Z | %c | %D | %F | %s | %x | %n | %t | %%', + 'd | m | y | Y | H | i | s | D | l | j | z | N | w | %U | W | %W | M | F | M | %C | y | Y | G | h | g | A | a | h:i:s A | H:i | H:i:s | %X | O | T | %c | m/d/y | Y-m-d | U | %x | %n | %t | %', + ], + ]; + } + /** * @dataProvider ToCamelCaseProvider * @covers utils::ToCamelCase diff --git a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php index 68d7df646..c8693ed24 100644 --- a/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/DBBackupTest.php @@ -4,6 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use CMDBSource; use Combodo\iTop\Test\UnitTest\ItopTestCase; +use DateTime; use DBBackup; use utils; @@ -102,4 +103,45 @@ class DBBackupTest extends ItopTestCase } $this->assertStringEndsWith('--ssl-ca='.DBBackup::EscapeShellArg($sTestCa), $sCliArgsCapathCfg); } + + /** + * @dataProvider MakeNameProvider + * @covers \DBBackup::MakeName + * + * @param string $sInputFormat + * @param \DateTime $oBackupDateTime + * @param string $sExpectedFilename + * + * @return void + */ + public function testMakeName(string $sInputFormat, DateTime $oBackupDateTime, string $sExpectedFilename): void + { + $oBackup = new DBBackup(utils::GetConfig()); + $sTestedFilename = $oBackup->MakeName($sInputFormat, $oBackupDateTime); + + $this->assertEquals($sExpectedFilename, $sTestedFilename, "Backup filename for '$sInputFormat' format doesn't match. Got '$sTestedFilename', expected '$sExpectedFilename'."); + } + + public function MakeNameProvider(): array + { + $oBackupDateTime = DateTime::createFromFormat('Y-m-d H:i:s', '1985-07-30 15:30:59'); + + return [ + 'Default format' => [ + 'itopdb-%Y-%m-%d', + $oBackupDateTime, + 'itopdb-1985-07-30', + ], + 'With time which is a placeholder that needs to be translated (minutes defined by "%M" when its actually "i" in the transformation matrix)' => [ + 'itopdb-%Y-%m-%d_%H:%M:%S', + $oBackupDateTime, + 'itopdb-1985-07-30_15:30:59', + ], + 'With user defined string that would be translated if using \DateTime::format() directly' => [ + 'itopdb-%Y-%m-%d-production', + $oBackupDateTime, + 'itopdb-1985-07-30-production', + ], + ]; + } }