From 2b9db7387199ef5cfa56fe2e2e2239a08f04cc35 Mon Sep 17 00:00:00 2001 From: v-dumas Date: Fri, 22 May 2026 16:13:19 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B09553=20-=20Helper=20for=20loading=20XML?= =?UTF-8?q?=20localized=20data=20during=20Setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.x/itop-tickets/module.itop-tickets.php | 43 +------- setup/moduleinstaller.class.inc.php | 90 +++++++++++++++ .../setup/ModuleInstallerAPITest.php | 104 +++++++++++++++++- 3 files changed, 198 insertions(+), 39 deletions(-) diff --git a/datamodels/2.x/itop-tickets/module.itop-tickets.php b/datamodels/2.x/itop-tickets/module.itop-tickets.php index 92fc730191..eeafa142bc 100755 --- a/datamodels/2.x/itop-tickets/module.itop-tickets.php +++ b/datamodels/2.x/itop-tickets/module.itop-tickets.php @@ -56,46 +56,13 @@ class TicketsInstaller extends ModuleInstallerAPI if (!MetaModel::IsValidClass($oTrigger->Get('target_class'))) { $oTrigger->DBDelete(); } - } catch (Exception $e) { + } + catch (Exception $e) { utils::EnrichRaisedException($oTrigger, $e); } } - // It's not very clear if it make sense to test a particular version, - // as the loading mechanism checks object existence using reconc_keys - // and do not recreate them, nor update existing. - // Without test, new entries added to the data files, would be automatically loaded - if (($sPreviousVersion === '') || - (version_compare($sPreviousVersion, $sCurrentVersion, '<') - && version_compare($sPreviousVersion, '3.0.0', '<'))) { - $oDataLoader = new XMLDataLoader(); - - CMDBObject::SetTrackInfo("Initialization TicketsInstaller"); - $oMyChange = CMDBObject::GetCurrentChange(); - - $sLang = null; - // - Try to get app. language from configuration fil (app. upgrade) - $sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE; - if (file_exists($sConfigFileName)) { - $oFileConfig = new Config($sConfigFileName); - if (is_object($oFileConfig)) { - $sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage())); - } - } - - // - I still no language, get the default one - if (null === $sLang) { - $sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage())); - } - - $sFileName = dirname(__FILE__)."/data/{$sLang}.data.itop-tickets.xml"; - SetupLog::Info("Searching file: $sFileName"); - if (!file_exists($sFileName)) { - $sFileName = dirname(__FILE__)."/data/en_us.data.itop-tickets.xml"; - } - SetupLog::Info("Loading file: $sFileName"); - $oDataLoader->StartSession($oMyChange); - $oDataLoader->LoadFile($sFileName, false, true); - $oDataLoader->EndSession(); - } + // Load localized structural data: predefined query phrases for notifications + static::LoadLocalizedData($sPreviousVersion, $sCurrentVersion, $oConfiguration, '3.0.0', dirname(__FILE__)."/data/{{language_code}}.data.itop-tickets.xml"); } } + diff --git a/setup/moduleinstaller.class.inc.php b/setup/moduleinstaller.class.inc.php index 0815fd4ba7..3cd0c6dac3 100644 --- a/setup/moduleinstaller.class.inc.php +++ b/setup/moduleinstaller.class.inc.php @@ -309,4 +309,94 @@ abstract class ModuleInstallerAPI CMDBSource::CacheReset($sOrigTable); } + + /** + * @param string $sPreviousVersion The previous version of the module (empty string will force the loading) + * @param string $sCurrentVersion The current version of the module + * @param \Config $oConfiguration + * @param string $sFirstLoadingVersion The first module version for which the data loading should be performed (e.g. '3.0.0') + * @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml') + * + * @return void + * @throws \ConfigException + * @throws \CoreException + */ + public static function LoadLocalizedData(string $sPreviousVersion, string $sCurrentVersion, Config $oConfiguration, string $sFirstLoadingVersion, string $sFilePattern): void + { + // It's not very clear if it makes sense to test a particular version, + // as the loading mechanism checks object existence using reconc_keys + // and do not recreate them, nor update existing. + // Without test, new entries added to the data files, would be automatically loaded + if (($sPreviousVersion === '') || + (version_compare($sPreviousVersion, $sCurrentVersion, '<') + && version_compare($sPreviousVersion, $sFirstLoadingVersion, '<'))) { + + $sFileName = self::GetLocalizedFileName($oConfiguration, $sFilePattern); + if ($sFileName !== '') { + SetupLog::Info("Loading file: $sFileName"); + self::XMLFileLoad($sFileName); + } + } + } + + /** + * @param array|string $sFileName + * @param \XMLDataLoader $oDataLoader + * + * @return void + * @throws \Exception + */ + public static function XMLFileLoad(string $sFileName): void + { + if (file_exists($sFileName)) { + $oDataLoader = new XMLDataLoader(); + CMDBObject::SetTrackInfo("Loading XML data from $sFileName"); + $oMyChange = CMDBObject::GetCurrentChange(); + SetupLog::Info("Loading file: $sFileName"); + $oDataLoader->StartSession($oMyChange); + $oDataLoader->LoadFile($sFileName, false, true); + $oDataLoader->EndSession(); + } + } + + /** + * @param \Config $oConfiguration + * @param string $sFilePattern The full path+name of the file to localize, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml') + * + * @return string The localized file name if found, or an empty string if not found + * @throws \ConfigException + * @throws \CoreException + */ + public static function GetLocalizedFileName(Config $oConfiguration, string $sFilePattern): string + { + $sLang = null; + if (is_object($oConfiguration)) { + $sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage())); + } + /** Old code relying on reading the file instead of using the configuration passed object + * Try to get app. language from configuration fil (app. upgrade) + $sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE; + if (file_exists($sConfigFileName)) { + $oFileConfig = new Config($sConfigFileName); + if (is_object($oFileConfig)) { + $sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage())); + } + } + **/ + // - I still no language, get the default one + if (null === $sLang) { + $sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage())); + } + $sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern); + if (!file_exists($sFileName)) { + $sLang = 'en_us'; + $sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern); + } + if (file_exists($sFileName)) { + return $sFileName; + } else { + SetupLog::Warning("No data file matching the pattern $sFilePattern and language_code $sLang was found."); + return ''; + } + } } diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleInstallerAPITest.php b/tests/php-unit-tests/unitary-tests/setup/ModuleInstallerAPITest.php index 7001c19fd6..54f470f3d0 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleInstallerAPITest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ModuleInstallerAPITest.php @@ -4,11 +4,12 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use CMDBSource; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; +use Config; use MetaModel; use ModuleInstallerAPI; /** - * Class ModuleInstallerAPITest + * Class ModuleInstallerAPI * * @covers ModuleInstallerAPI * @@ -282,4 +283,105 @@ SQL $this->assertEquals($sOrigValue, $sDstValue, "Data was not moved as expected"); } + + /** + * @covers \ModuleInstallerAPI::LoadLocalizedData + */ + public function testLoadLocalizedData_LoadsOnFirstInstall(): void + { + // Given + [$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_FirstInstall_', 'fr_fr'); + $this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName); + $this->CreateLocalizedDataFile($sTmpDir, "fr_fr", $sOrgName); + // When no previous version, and current version higher than the first loading version + ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern); + // Then data loaded + $this->AssertOrganizationCountByName($sOrgName, 'en_us', 0); + $this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 1); + } + + /** + * @covers \ModuleInstallerAPI::LoadLocalizedData + */ + public function testLoadLocalizedData_DoesNotLoadWhenVersionConditionIsNotMet(): void + { + // Given + [$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_NoLoad_', 'en_us'); + $this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName); + + // When a previous version that is lower than the first loading version, but higher or equal to the current version + ModuleInstallerAPI::LoadLocalizedData('3.0.0', '3.1.0', $oConfig, '3.0.0', $sPattern); + // Then no data loaded + $this->AssertOrganizationCountByName($sOrgName, 'en_us', 0); + } + + /** + * @covers \ModuleInstallerAPI::LoadLocalizedData + */ + public function testLoadLocalizedData_FallbacksToEnUsWhenLanguageFileIsMissing(): void + { + [$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_Fallback_', 'fr_fr'); + // Intentionally create ONLY en_us file + $this->CreateLocalizedDataFile($sTmpDir, 'en_us', $sOrgName); + // When loading localized data in fr_fr, but only en_us file exists + ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern); + + $this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 0); + $this->AssertOrganizationCountByName($sOrgName, 'en_us', 1); + } + + /** + * Prepare common context for LoadLocalizedData tests. + * + * @return array{0: Config, 1: string, 2: string, 3: string, 4: string} + */ + private function PrepareLocalizedDataTestContext(string $sOrgNamePrefix, string $sLanguage): array + { + $oConfig = MetaModel::GetConfig(); + $oConfig->SetDefaultLanguage($sLanguage); + $this->assertNotNull($oConfig); + + $sOrgName = $sOrgNamePrefix.uniqid(); + + $sTmpDir = static::CreateTmpdir(); + $this->aFileToClean[] = $sTmpDir; + $sPattern = $sTmpDir.DIRECTORY_SEPARATOR.'data.{{language_code}}.xml'; + + return [$oConfig, $sOrgName, $sTmpDir, $sPattern]; + } + + private function CreateLocalizedDataFile(string $sDir, string $sLang, string $sOrgName): string + { + $sFilePath = $sDir.DIRECTORY_SEPARATOR.'data.'.$sLang.'.xml'; + file_put_contents($sFilePath, $this->BuildOrganizationXml($sOrgName, $sLang)); + + return $sFilePath; + } + + private function BuildOrganizationXml(string $sOrgName, string $sLang): string + { + $iId = random_int(100000, 999999); + $sOrgNameXml = htmlspecialchars($sOrgName, ENT_XML1); + + return << + + + {$sOrgNameXml} + {$sLang} + active + + +XML; + } + + private function AssertOrganizationCountByName(string $sOrgName, string $sLanguage, int $iExpectedCount): void + { + $sOrgTable = MetaModel::DBGetTable('Organization'); + $iCount = (int) CMDBSource::QueryToScalar( + "SELECT COUNT(*) FROM `{$sOrgTable}` WHERE `name` = ".CMDBSource::Quote($sOrgName)." AND `code` = ".CMDBSource::Quote($sLanguage) + ); + + $this->assertEquals($iExpectedCount, $iCount); + } }