From a2b0ad6c11e68a76056eaddd463b346633ff0c82 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 15 Dec 2025 16:25:46 +0100 Subject: [PATCH 01/16] ci: fix broken LoginTest --- tests/php-unit-tests/unitary-tests/application/LoginTest.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/application/LoginTest.php diff --git a/tests/php-unit-tests/unitary-tests/application/LoginTest.php b/tests/php-unit-tests/unitary-tests/application/LoginTest.php new file mode 100644 index 0000000000..e69de29bb2 From 1ef4462517b2ac8901548deaee5f97fb8f42017f Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 10 Dec 2025 10:59:13 +0100 Subject: [PATCH 02/16] =?UTF-8?q?N=C2=B08981=20-=20cleanup/simplify=20setu?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix RecordInstallation due to previous refactoring --- .../src/Service/CoreUpdater.php | 19 ++---- datamodels/2.x/itop-hub-connector/ajax.php | 21 ++---- setup/applicationinstaller.class.inc.php | 10 +-- setup/runtimeenv.class.inc.php | 64 +++++++++++-------- 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php index 05a1f43a7c..3fac3040db 100644 --- a/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php +++ b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php @@ -92,7 +92,7 @@ final class CoreUpdater $sFinalEnv = 'production'; $oRuntimeEnv = new RunTimeEnvironmentCoreUpdater($sFinalEnv, false); $oRuntimeEnv->CheckDirectories($sFinalEnv); - $oRuntimeEnv->CompileFrom('production'); + $oRuntimeEnv->CompileFrom($sFinalEnv); $oRuntimeEnv->Rollback(); @@ -156,20 +156,13 @@ final class CoreUpdater ]; $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $aDirsToScanForModules); $aSelectedModules = []; - foreach ($aAvailableModules as $sModuleId => $aModule) { - if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { - continue; - } else { - $aSelectedModules[] = $sModuleId; - } - } - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); $oRuntimeEnv->UpdatePredefinedObjects(); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup'); - $oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); + $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); $oExtensionsMap = new iTopExtensionsMap(); // Default choices = as before diff --git a/datamodels/2.x/itop-hub-connector/ajax.php b/datamodels/2.x/itop-hub-connector/ajax.php index 33ac5940b1..b7fe109b7c 100644 --- a/datamodels/2.x/itop-hub-connector/ajax.php +++ b/datamodels/2.x/itop-hub-connector/ajax.php @@ -293,28 +293,19 @@ try { $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); - $aSelectedModules = []; - foreach ($aAvailableModules as $sModuleId => $aModule) { - if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { - continue; - } else { - $aSelectedModules[] = $sModuleId; - } - } - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); $oRuntimeEnv->UpdatePredefinedObjects(); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); - $oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/); + $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); // Record the installation so that the "about box" knows about the installed modules $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); @@ -334,7 +325,7 @@ try { $aSelectedExtensionCodes[] = $oExtension->sCode; } $aSelectedExtensions = $oExtensionsMap->GetChoices(); - $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); + $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); // Report the success in a way that will be detected by the ajax caller SetupLog::Info('Deployment successfully completed.'); diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index ca62935653..bb9b9de627 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -743,7 +743,7 @@ class ApplicationInstaller // Module specific actions (migrate the data) // $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); - $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation'); + $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules); if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) { throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'"); @@ -873,7 +873,7 @@ class ApplicationInstaller // Perform here additional DB setup... profiles, etc... // $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); - $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation'); + $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules); $oProductionEnv->UpdatePredefinedObjects(); @@ -887,7 +887,7 @@ class ApplicationInstaller // Perform final setup tasks here // - $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup'); + $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules); } /** @@ -937,11 +937,11 @@ class ApplicationInstaller } $aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir); - $oProductionEnv->LoadData($aAvailableModules, $aSelectedModules, $bSampleData); + $oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules); // Perform after dbload setup tasks here // - $oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad'); + $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules); } /** diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 2dd8a66d39..3b857dbd16 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -548,10 +548,15 @@ class RunTimeEnvironment // Record installed modules and extensions // $aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir()); - foreach ($aSelectedModuleCodes as $sModuleId) { + foreach ($aSelectedModuleCodes as $sModuleId => $aData) { + if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { + continue; + } + if (!array_key_exists($sModuleId, $aAvailableModules)) { continue; } + $aModuleData = $aAvailableModules[$sModuleId]; $sName = $sModuleId; $sVersion = $aModuleData['available_version']; @@ -851,14 +856,18 @@ class RunTimeEnvironment /** * Call the given handler method for all selected modules having an installation handler * @param array[] $aAvailableModules - * @param string[] $aSelectedModules * @param string $sHandlerName + * @param string[]|null $aSelectedModules * @throws CoreException */ - public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName) + public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null) { foreach ($aAvailableModules as $sModuleId => $aModule) { - if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) { + if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { + continue; + } + + if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) { $aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']]; RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs); } @@ -903,10 +912,10 @@ class RunTimeEnvironment /** * Load data from XML files for the selected modules (structural data and/or sample data) * @param array[] $aAvailableModules All available modules and their definition - * @param string[] $aSelectedModules List of selected modules * @param bool $bSampleData Wether or not to load sample data + * @param null|string[] $aSelectedModules List of selected modules */ - public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData) + public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules=null) { $oDataLoader = new XMLDataLoader(); @@ -919,30 +928,33 @@ class RunTimeEnvironment $aFiles = []; $aPreviouslyLoadedFiles = []; foreach ($aAvailableModules as $sModuleId => $aModule) { - if (($sModuleId != ROOT_MODULE)) { - $sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']); - // Load data only for selected AND newly installed modules - if (in_array($sModuleId, $aSelectedModules)) { - if ($aModule['installed_version'] != '') { - // Simulate the load of the previously loaded XML files to get the mapping of the keys - if ($bSampleData) { - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } else { - // Load only structural data - $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - } + if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { + continue; + } + + $sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']); + // Load data only for selected AND newly installed modules + if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) { + if ($aModule['installed_version'] != '') { + // Simulate the load of the previously loaded XML files to get the mapping of the keys + if ($bSampleData) { + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); } else { - if ($bSampleData) { - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); - } else { - // Load only structural data - $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); - } + // Load only structural data + $aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + } + } else { + if ($bSampleData) { + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']); + } else { + // Load only structural data + $aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']); } } } + } // Simulate the load of the previously loaded files, in order to initialize From d33ca81198698c33c3cb2c55fbe23680580954d2 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 10 Dec 2025 16:46:51 +0100 Subject: [PATCH 03/16] =?UTF-8?q?N=C2=B04789=20-=20do=20not=20call=20empty?= =?UTF-8?q?=20ModuleInstallerAPI=20class=20+=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit code style --- setup/modulediscovery/ModuleFileReader.php | 6 ++++++ setup/runtimeenv.class.inc.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/setup/modulediscovery/ModuleFileReader.php b/setup/modulediscovery/ModuleFileReader.php index 118f2249ac..00cb7158ea 100644 --- a/setup/modulediscovery/ModuleFileReader.php +++ b/setup/modulediscovery/ModuleFileReader.php @@ -175,15 +175,21 @@ class ModuleFileReader } $sModuleInstallerClass = $aModuleInfo['installer']; + if (strlen($sModuleInstallerClass) === 0) { + return null; + } + if (!class_exists($sModuleInstallerClass)) { $sModuleFilePath = $aModuleInfo['module_file_path']; $this->ReadModuleFileInformationUnsafe($sModuleFilePath); } if (!class_exists($sModuleInstallerClass)) { + \IssueLog::Error(__METHOD__, null, $aModuleInfo); throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']); } if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) { + \IssueLog::Error(__METHOD__, null, $aModuleInfo); throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']); } diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 3b857dbd16..d57aaeebc5 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -915,7 +915,7 @@ class RunTimeEnvironment * @param bool $bSampleData Wether or not to load sample data * @param null|string[] $aSelectedModules List of selected modules */ - public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules=null) + public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null) { $oDataLoader = new XMLDataLoader(); From 85e28931f5a915db4bd8f7bfef78a68432d25c7a Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 17 Dec 2025 18:11:34 +0100 Subject: [PATCH 04/16] =?UTF-8?q?N=C2=B08981:=20prepare=20hub=20connector?= =?UTF-8?q?=20test=20cover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdk test enhancement : add call itop api fix ci ci: fix broken tests ci: cover hub setup on compile and launch steps code style ci: fix ModuleDiscoveryTest redundant class + add logs to investigate ci setup issues ci: fix log during setup tests --- datamodels/2.x/itop-hub-connector/ajax.php | 269 +--------------- .../src/Controller/HubController.php | 300 ++++++++++++++++++ .../src/Model/DBBackupWithErrorReporting.php | 37 +++ .../hubruntimeenvironment.class.inc.php | 13 + sources/Application/WebPage/JsonPage.php | 19 +- .../itop-hub-connector/HubControllerTest.php | 83 +++++ .../Service/UnitTestRunTimeEnvironment.php | 10 +- .../modulediscovery/ModuleDiscoveryTest.php | 50 --- .../setup/ModuleDiscoveryTest.php | 34 ++ 9 files changed, 504 insertions(+), 311 deletions(-) create mode 100644 datamodels/2.x/itop-hub-connector/src/Controller/HubController.php create mode 100644 datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php rename datamodels/2.x/itop-hub-connector/{ => src/setup}/hubruntimeenvironment.class.inc.php (94%) create mode 100644 tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php delete mode 100644 tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php diff --git a/datamodels/2.x/itop-hub-connector/ajax.php b/datamodels/2.x/itop-hub-connector/ajax.php index b7fe109b7c..23559f9e54 100644 --- a/datamodels/2.x/itop-hub-connector/ajax.php +++ b/datamodels/2.x/itop-hub-connector/ajax.php @@ -24,129 +24,13 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -use Combodo\iTop\Application\WebPage\JsonPage; +use Combodo\iTop\HubConnector\Controller\HubController; require_once(APPROOT.'application/utils.inc.php'); require_once(APPROOT.'core/log.class.inc.php'); IssueLog::Enable(APPROOT.'log/error.log'); -require_once(APPROOT.'setup/runtimeenv.class.inc.php'); -require_once(APPROOT.'setup/backup.class.inc.php'); -require_once(APPROOT.'core/mutex.class.inc.php'); -require_once(APPROOT.'core/dict.class.inc.php'); -require_once(APPROOT.'setup/xmldataloader.class.inc.php'); -require_once(__DIR__.'/hubruntimeenvironment.class.inc.php'); - -/** - * Overload of DBBackup to handle logging - */ -class DBBackupWithErrorReporting extends DBBackup -{ - protected $aInfos = []; - - protected $aErrors = []; - - protected function LogInfo($sMsg) - { - $aInfos[] = $sMsg; - } - - protected function LogError($sMsg) - { - IssueLog::Error($sMsg); - $aErrors[] = $sMsg; - } - - public function GetInfos() - { - return $this->aInfos; - } - - public function GetErrors() - { - return $this->aErrors; - } -} - -/** - * - * @param string $sTargetFile - * @throws Exception - * @return DBBackupWithErrorReporting - */ -function DoBackup($sTargetFile) -{ - // Make sure the target directory exists - $sBackupDir = dirname($sTargetFile); - SetupUtils::builddir($sBackupDir); - - $oBackup = new DBBackupWithErrorReporting(); - $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); - $sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE; - - $oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment()); - $oMutex->Lock(); - try { - $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); - } catch (Exception $e) { - $oMutex->Unlock(); - throw $e; - } - $oMutex->Unlock(); - return $oBackup; -} - -/** - * Outputs the status of the current ajax execution (as a JSON structure) - * - * @param string $sMessage - * @param bool $bSuccess - * @param number $iErrorCode - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = []) -{ - // Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation - $oPage = new JsonPage(); - $aResult = [ - 'code' => $iErrorCode, - 'message' => $sMessage, - 'fields' => $aMoreFields, - ]; - $oPage->SetData($aResult); - $oPage->SetOutputDataOnly(true); - $oPage->output(); -} - -/** - * Helper to output the status of a successful execution - * - * @param string $sMessage - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportSuccess($sMessage, $aMoreFields = []) -{ - ReportStatus($sMessage, true, 0, $aMoreFields); -} - -/** - * Helper to output the status of a failed execution - * - * @param string $sMessage - * @param number $iErrorCode - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportError($sMessage, $iErrorCode, $aMoreFields = []) -{ - if ($iErrorCode == 0) { - // 0 means no error, so change it if no meaningful error code is supplied - $iErrorCode = -1; - } - ReportStatus($sMessage, false, $iErrorCode, $aMoreFields); -} +require_once(__DIR__.'/src/Controller/HubController.php'); try { SetupUtils::ExitMaintenanceMode(false); // Reset maintenance mode in case of problem @@ -183,7 +67,7 @@ try { foreach ($aChecks as $oCheckResult) { if ($oCheckResult->iSeverity == CheckResult::ERROR) { $bFailed = true; - ReportError($oCheckResult->sLabel, -2); + HubController::GetInstance()->ReportError($oCheckResult->sLabel, -2); } } if (!$bFailed) { @@ -191,160 +75,27 @@ try { $fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath); if ($fFreeSpace !== false) { $sMessage = Dict::Format('iTopHub:BackupFreeDiskSpaceIn', SetupUtils::HumanReadableSize($fFreeSpace), dirname($sDBBackupPath)); - ReportSuccess($sMessage); + HubController::GetInstance()->ReportSuccess($sMessage); } else { - ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1); + HubController::GetInstance()->ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1); } } break; case 'do_backup': - require_once(APPROOT.'/application/startup.inc.php'); - require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) - - try { - if (MetaModel::GetConfig()->Get('demo_mode')) { - throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); - } - SetupLog::Info('Backup starts...'); - set_time_limit(0); - $sBackupPath = APPROOT.'/data/backups/manual/backup-'; - $iSuffix = 1; - $sSuffix = ''; - // Generate a unique name... - do { - $sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix; - $sSuffix = '-'.$iSuffix; - $iSuffix++ ; - } while (file_exists($sBackupFile)); - - $oBackup = DoBackup($sBackupFile); - $aErrors = $oBackup->GetErrors(); - if (count($aErrors) > 0) { - SetupLog::Error('Backup failed.'); - SetupLog::Error(implode("\n", $aErrors)); - ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors); - } else { - SetupLog::Info('Backup successfully completed.'); - ReportSuccess(Dict::S('iTopHub:BackupOk')); - } - } catch (Exception $e) { - SetupLog::Error($e->getMessage()); - ReportError($e->getMessage(), $e->getCode()); - } + HubController::GetInstance()->LaunchBackup(); break; case 'compile': - SetupLog::Info('Deployment starts...'); - $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); - if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { - throw new SecurityException(Dict::S('iTopHub:FailAuthent')); - } - // First step: prepare the datamodel, if it fails, roll-back - $aSelectedExtensionCodes = utils::ReadParam('extension_codes', []); - $aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []); - - $oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build - $oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs); - - $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); - if ($oConfig->Get('demo_mode')) { - throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); - } - - $aSelectModules = $oRuntimeEnv->CompileFrom('production', false); // WARNING symlinks does not seem to be compatible with manual Commit - - $oRuntimeEnv->UpdateIncludes($oConfig); - - $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); - - // Safety check: check the inter dependencies, will throw an exception in case of inconsistency - $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); - - $oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected - - // Everything seems Ok so far, commit in env-production! - $oRuntimeEnv->WriteConfigFileSafe($oConfig); - $oRuntimeEnv->Commit(); - - // Report the success in a way that will be detected by the ajax caller - SetupLog::Info('Compilation completed...'); - ReportSuccess('Ok'); // No access to Dict::S here + HubController::GetInstance()->LaunchCompile(); break; case 'move_to_production': - // Second step: update the schema and the data - // Everything happening below is based on env-production - $oRuntimeEnv = new RunTimeEnvironment('production', true); - - try { - SetupLog::Info('Move to production starts...'); - $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); - if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { - throw new SecurityException(Dict::S('iTopHub:FailAuthent')); - } - unlink(utils::GetDataPath().'hub/compile_authent'); - // Load the "production" config file to clone & update it - $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); - SetupUtils::EnterReadOnlyMode($oConfig); - - $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); - - $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); - - $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); - - $oRuntimeEnv->UpdatePredefinedObjects(); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); - - $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); - - // Record the installation so that the "about box" knows about the installed modules - $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); - - $oExtensionsMap = new iTopExtensionsMap(); - - // Default choices = as before - $oExtensionsMap->LoadChoicesFromDatabase($oConfig); - foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) { - // Plus all "remote" extensions - if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) { - $oExtensionsMap->MarkAsChosen($oExtension->sCode); - } - } - $aSelectedExtensionCodes = []; - foreach ($oExtensionsMap->GetChoices() as $oExtension) { - $aSelectedExtensionCodes[] = $oExtension->sCode; - } - $aSelectedExtensions = $oExtensionsMap->GetChoices(); - $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); - - // Report the success in a way that will be detected by the ajax caller - SetupLog::Info('Deployment successfully completed.'); - ReportSuccess(Dict::S('iTopHub:CompiledOK')); - } catch (Exception $e) { - if (file_exists(utils::GetDataPath().'hub/compile_authent')) { - unlink(utils::GetDataPath().'hub/compile_authent'); - } - // Note: at this point, the dictionnary is not necessarily loaded - SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); - SetupLog::Error('Debug trace: '.$e->getTraceAsString()); - ReportError($e->getMessage(), $e->getCode()); - } finally { - SetupUtils::ExitReadOnlyMode(); - } + HubController::GetInstance()->LaunchDeploy(); break; default: - ReportError("Invalid operation: '$sOperation'", -1); + HubController::GetInstance()->ReportError("Invalid operation: '$sOperation'", -1); } } catch (Exception $e) { SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); @@ -352,5 +103,5 @@ try { utils::PopArchiveMode(); - ReportError($e->getMessage(), $e->getCode()); + HubController::GetInstance()->ReportError($e->getMessage(), $e->getCode()); } diff --git a/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php new file mode 100644 index 0000000000..fc0fccdaec --- /dev/null +++ b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php @@ -0,0 +1,300 @@ +Get('demo_mode')) { + throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); + } + SetupLog::Info('Backup starts...'); + set_time_limit(0); + $sBackupPath = APPROOT.'/data/backups/manual/backup-'; + $iSuffix = 1; + $sSuffix = ''; + // Generate a unique name... + do { + $sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix; + $sSuffix = '-'.$iSuffix; + $iSuffix++ ; + } while (file_exists($sBackupFile)); + + $oBackup = $this->DoBackup($sBackupFile); + $aErrors = $oBackup->GetErrors(); + if (count($aErrors) > 0) { + SetupLog::Error('Backup failed.'); + SetupLog::Error(implode("\n", $aErrors)); + $this->ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors); + } else { + SetupLog::Info('Backup successfully completed.'); + $this->ReportSuccess(Dict::S('iTopHub:BackupOk')); + } + } catch (Exception $e) { + SetupLog::Error($e->getMessage()); + $this->ReportError($e->getMessage(), $e->getCode()); + } + } + + /** + * + * @param string $sTargetFile + * @throws Exception + * @return DBBackupWithErrorReporting + */ + public function DoBackup($sTargetFile): DBBackupWithErrorReporting + { + // Make sure the target directory exists + $sBackupDir = dirname($sTargetFile); + SetupUtils::builddir($sBackupDir); + + $oBackup = new DBBackupWithErrorReporting(); + $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); + $sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE; + + $oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment()); + $oMutex->Lock(); + try { + $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); + } catch (Exception $e) { + $oMutex->Unlock(); + throw $e; + } + $oMutex->Unlock(); + return $oBackup; + } + + public function LaunchCompile() + { + SetupLog::Info('Deployment starts...'); + $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); + if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { + throw new SecurityException(Dict::S('iTopHub:FailAuthent')); + } + // First step: prepare the datamodel, if it fails, roll-back + $aSelectedExtensionCodes = utils::ReadParam('extension_codes', []); + $aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []); + + $oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build + $oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs); + + $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); + if ($oConfig->Get('demo_mode')) { + throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); + } + + $aSelectModules = $oRuntimeEnv->CompileFrom('production'); // WARNING symlinks does not seem to be compatible with manual Commit + + $oRuntimeEnv->UpdateIncludes($oConfig); + + $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); + + // Safety check: check the inter dependencies, will throw an exception in case of inconsistency + $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); + + $oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected + + // Everything seems Ok so far, commit in env-production! + $oRuntimeEnv->WriteConfigFileSafe($oConfig); + $oRuntimeEnv->Commit(); + + // Report the success in a way that will be detected by the ajax caller + SetupLog::Info('Compilation completed...'); + + $this->ReportSuccess('Ok'); // No access to Dict::S here + } + + public function LaunchDeploy() + { + // Second step: update the schema and the data + // Everything happening below is based on env-production + $oRuntimeEnv = new RunTimeEnvironment('production', true); + + try { + SetupLog::Info('Move to production starts...'); + $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); + if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { + throw new SecurityException(Dict::S('iTopHub:FailAuthent')); + } + unlink(utils::GetDataPath().'hub/compile_authent'); + // Load the "production" config file to clone & update it + $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); + SetupUtils::EnterReadOnlyMode($oConfig); + + $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); + + $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); + + $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); + + $oRuntimeEnv->UpdatePredefinedObjects(); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); + + $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); + + // Record the installation so that the "about box" knows about the installed modules + $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); + + $oExtensionsMap = new iTopExtensionsMap(); + + // Default choices = as before + $oExtensionsMap->LoadChoicesFromDatabase($oConfig); + foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) { + // Plus all "remote" extensions + if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) { + $oExtensionsMap->MarkAsChosen($oExtension->sCode); + } + } + $aSelectedExtensionCodes = []; + foreach ($oExtensionsMap->GetChoices() as $oExtension) { + $aSelectedExtensionCodes[] = $oExtension->sCode; + } + $aSelectedExtensions = $oExtensionsMap->GetChoices(); + $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); + + // Report the success in a way that will be detected by the ajax caller + SetupLog::Info('Deployment successfully completed.'); + $this->ReportSuccess(Dict::S('iTopHub:CompiledOK')); + } catch (Exception $e) { + if (file_exists(utils::GetDataPath().'hub/compile_authent')) { + unlink(utils::GetDataPath().'hub/compile_authent'); + } + // Note: at this point, the dictionnary is not necessarily loaded + SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); + SetupLog::Error('Debug trace: '.$e->getTraceAsString()); + $this->ReportError($e->getMessage(), $e->getCode()); + } finally { + SetupUtils::ExitReadOnlyMode(); + } + } + + /** + * Outputs the status of the current ajax execution (as a JSON structure) + * + * @param string $sMessage + * @param bool $bSuccess + * @param number $iErrorCode + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = []) + { + // Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation + $this->oLastJsonPage = new JsonPage(); + $this->oLastJsonPage->SetOutputHeaders($this->bOutputHeaders); + $aResult = [ + 'code' => $iErrorCode, + 'message' => $sMessage, + 'fields' => $aMoreFields, + ]; + $this->oLastJsonPage->SetData($aResult); + $this->oLastJsonPage->SetOutputDataOnly(true); + $this->oLastJsonPage->output(); + } + + private ?JsonPage $oLastJsonPage = null; + + public function GetLastJsonPage(): ?JsonPage + { + return $this->oLastJsonPage; + } + + /** + * Helper to output the status of a successful execution + * + * @param string $sMessage + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportSuccess($sMessage, $aMoreFields = []) + { + $this->ReportStatus($sMessage, true, 0, $aMoreFields); + } + + /** + * Helper to output the status of a failed execution + * + * @param string $sMessage + * @param number $iErrorCode + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportError($sMessage, $iErrorCode, $aMoreFields = []) + { + if ($iErrorCode == 0) { + // 0 means no error, so change it if no meaningful error code is supplied + $iErrorCode = -1; + } + $this->ReportStatus($sMessage, false, $iErrorCode, $aMoreFields); + } + + /** + * Dont print headers for testing purpose mainly + * @param bool bOutputHeaders + * + * @return void + */ + public function SetOutputHeaders(bool $bOutputHeaders): void + { + $this->bOutputHeaders = $bOutputHeaders; + } +} diff --git a/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php b/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php new file mode 100644 index 0000000000..39bcabe856 --- /dev/null +++ b/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php @@ -0,0 +1,37 @@ +aInfos[] = $sMsg; + } + + protected function LogError($sMsg) + { + IssueLog::Error($sMsg); + $this->aErrors[] = $sMsg; + } + + public function GetInfos(): array + { + return $this->aInfos; + } + + public function GetErrors(): array + { + return $this->aErrors; + } +} diff --git a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php b/datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php similarity index 94% rename from datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php rename to datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php index c5c722cf77..8d848d9673 100644 --- a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php +++ b/datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php @@ -1,9 +1,17 @@ -modules + * * @param string $sDownloadedExtensionsDir The directory to scan * @param string[] $aSelectedExtensionDirs The list of folders to move + * * @throws Exception */ public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs) diff --git a/sources/Application/WebPage/JsonPage.php b/sources/Application/WebPage/JsonPage.php index 4f8895a818..15da52b65b 100644 --- a/sources/Application/WebPage/JsonPage.php +++ b/sources/Application/WebPage/JsonPage.php @@ -25,6 +25,7 @@ class JsonPage extends WebPage * This can be useful when feeding response to a third party lib that doesn't understand the structured format. */ protected $bOutputDataOnly = false; + protected $bOutputHeaders = true; /** * JsonPage constructor. @@ -82,6 +83,19 @@ class JsonPage extends WebPage return $this; } + /** + * @see static::$bOutputHeaders + * @param bool $bFlag + * + * @return $this + */ + public function SetOutputHeaders(bool $bFlag) + { + $this->bOutputHeaders = $bFlag; + + return $this; + } + /** * Output the headers * @@ -119,7 +133,10 @@ class JsonPage extends WebPage public function output() { $oKpi = new ExecutionKPI(); - $this->OutputHeaders(); + if ($this->bOutputHeaders) { + $this->OutputHeaders(); + } + $sContent = $this->ComputeContent(); $oKpi->ComputeAndReport(get_class($this).' output'); diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php new file mode 100644 index 0000000000..fab403e503 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php @@ -0,0 +1,83 @@ +SkipIfModuleNotPresent('itop-hub-connector'); + parent::setUp(); + $this->RequireOnceItopFile('env-production/itop-hub-connector/src/Controller/HubController.php'); + } + + public function testLaunchCompile(): void + { + $this->PrepareCompileAuthent(); + + $this->CopyProductionModulesIntoHubExtensionDir(); + + touch(MFCompiler::USE_SYMBOLIC_LINKS_FILE_PATH); + HubController::GetInstance()->SetOutputHeaders(false); + + try { + HubController::GetInstance()->LaunchCompile(); + } catch (DOMFormatException $e) { + SetupLog::Error(__METHOD__, null, [ 'production.delta.xml' => @file_get_contents(APPROOT.'data/production.delta.xml')]); + throw $e; + } + + $this->CheckReport('{"code":0,"message":"Ok","fields":[]}'); + } + + public function testLaunchDeploy(): void + { + $this->PrepareCompileAuthent(); + HubController::GetInstance()->LaunchDeploy(); + $this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}'); + } + + private function CheckReport($sExpected) + { + $oJsonPage = HubController::GetInstance()->GetLastJsonPage(); + $this->assertEquals($sExpected, $this->InvokeNonPublicMethod(JsonPage::class, 'ComputeContent', $oJsonPage, [])); + + //keep line below to avoid: Test code or tested code did not (only) close its own output buffers + $this->InvokeNonPublicMethod(JsonPage::class, 'RenderContent', $oJsonPage, []); + } + + private function PrepareCompileAuthent() + { + $sUUID = 'hub_'.uniqid(); + $_REQUEST['authent'] = $sUUID; + $sPath = utils::GetDataPath().'hub/compile_authent'; + file_put_contents($sPath, $sUUID); + $this->aFileToClean[] = $sPath; + } + + private function CopyProductionModulesIntoHubExtensionDir() + { + $sProdModules = APPROOT.'/data/production-modules'; + $sExtensionDir = APPROOT.'/data/downloaded-extensions/'; + $this->aFileToClean[] = $sExtensionDir; + + SetupUtils::rrmdir($sExtensionDir); + SetupUtils::copydir($sExtensionDir, $sProdModules); + } +} diff --git a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php index d486b582ec..98fdd26fb8 100644 --- a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php +++ b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php @@ -8,11 +8,13 @@ namespace Combodo\iTop\Test\UnitTest\Service; use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase; +use DOMFormatException; use MFCoreModule; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; use RunTimeEnvironment; +use SetupLog; use SetupUtils; use utils; @@ -64,7 +66,13 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment } } - parent::CompileFrom($sSourceEnv, $bUseSymLinks); + try { + parent::CompileFrom($sSourceEnv, $bUseSymLinks); + } catch (DOMFormatException $e) { + $sFileName = $sSourceEnv.'.delta.xml'; + SetupLog::Error(__METHOD__, null, [$sFileName => @file_get_contents(APPROOT.'data/'.$sFileName)]); + throw $e; + } } public function IsUpToDate() diff --git a/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php deleted file mode 100644 index d863c6d64f..0000000000 --- a/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php +++ /dev/null @@ -1,50 +0,0 @@ - [ - 'sModuleId' => 'a/1.2.3', - 'name' => 'a', - 'version' => '1.2.3', - ], - 'develop' => [ - 'sModuleId' => 'a/1.2.3-dev', - 'name' => 'a', - 'version' => '1.2.3-dev', - ], - 'missing version => 1.0.0' => [ - 'sModuleId' => 'a/', - 'name' => 'a', - 'version' => '1.0.0', - ], - 'missing everything except name' => [ - 'sModuleId' => 'a', - 'name' => 'a', - 'version' => '1.0.0', - ], - ]; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); - } - - /** - * @dataProvider GetModuleNameProvider - */ - public function testGetModuleName($sModuleId, $expectedName, $expectedVersion) - { - $this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId)); - } - -} diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php index 4e31921c9e..badaaafb67 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php @@ -77,4 +77,38 @@ TXT; ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices); } + + public function GetModuleNameProvider() + { + return [ + 'nominal' => [ + 'sModuleId' => 'a/1.2.3', + 'name' => 'a', + 'version' => '1.2.3', + ], + 'develop' => [ + 'sModuleId' => 'a/1.2.3-dev', + 'name' => 'a', + 'version' => '1.2.3-dev', + ], + 'missing version => 1.0.0' => [ + 'sModuleId' => 'a/', + 'name' => 'a', + 'version' => '1.0.0', + ], + 'missing everything except name' => [ + 'sModuleId' => 'a', + 'name' => 'a', + 'version' => '1.0.0', + ], + ]; + } + + /** + * @dataProvider GetModuleNameProvider + */ + public function testGetModuleName($sModuleId, $expectedName, $expectedVersion) + { + $this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId)); + } } From 83973d102fbb649dfcc3c2637897f38dc2f6fb0c Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 29 Dec 2025 07:47:06 +0100 Subject: [PATCH 05/16] =?UTF-8?q?N=C2=B08981:=20repair=20previous=20setup?= =?UTF-8?q?=20cleanup=20(broken=20setups)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.x/itop-core-update/src/Service/CoreUpdater.php | 3 +-- .../src/Controller/HubController.php | 2 +- setup/runtimeenv.class.inc.php | 5 +++-- .../itop-hub-connector/HubControllerTest.php | 11 +++-------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php index 3fac3040db..b605248669 100644 --- a/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php +++ b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php @@ -155,7 +155,6 @@ final class CoreUpdater APPROOT.'extensions', ]; $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $aDirsToScanForModules); - $aSelectedModules = []; $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); @@ -180,7 +179,7 @@ final class CoreUpdater $oRuntimeEnv->RecordInstallation( $oConfig, $sDataModelVersion, - $aSelectedModules, + array_keys($aAvailableModules), $aSelectedExtensionCodes, 'Done by the iTop Core Updater' ); diff --git a/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php index fc0fccdaec..0890891e3a 100644 --- a/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php +++ b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php @@ -209,7 +209,7 @@ class HubController $aSelectedExtensionCodes[] = $oExtension->sCode; } $aSelectedExtensions = $oExtensionsMap->GetChoices(); - $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); + $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, array_keys($aAvailableModules), $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); // Report the success in a way that will be detected by the ajax caller SetupLog::Info('Deployment successfully completed.'); diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index d57aaeebc5..240d6e43bc 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -548,7 +548,7 @@ class RunTimeEnvironment // Record installed modules and extensions // $aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir()); - foreach ($aSelectedModuleCodes as $sModuleId => $aData) { + foreach ($aSelectedModuleCodes as $sModuleId) { if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) { continue; } @@ -869,7 +869,7 @@ class RunTimeEnvironment if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) { $aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']]; - RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs); + RunTimeEnvironment::CallInstallerHandler($aModule, $sHandlerName, $aArgs); } } } @@ -896,6 +896,7 @@ class RunTimeEnvironment try { call_user_func_array($aCallSpec, $aArgs); } catch (Exception $e) { + $sModuleId = isset($sModuleId) ? $sModuleId : ""; $sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler"; $aExceptionContextData = [ 'ModulelId' => $sModuleId, diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php index fab403e503..fc34a62082 100644 --- a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php @@ -33,22 +33,16 @@ class HubControllerTest extends ItopDataTestCase $this->CopyProductionModulesIntoHubExtensionDir(); - touch(MFCompiler::USE_SYMBOLIC_LINKS_FILE_PATH); HubController::GetInstance()->SetOutputHeaders(false); - try { - HubController::GetInstance()->LaunchCompile(); - } catch (DOMFormatException $e) { - SetupLog::Error(__METHOD__, null, [ 'production.delta.xml' => @file_get_contents(APPROOT.'data/production.delta.xml')]); - throw $e; - } + HubController::GetInstance()->LaunchCompile(); $this->CheckReport('{"code":0,"message":"Ok","fields":[]}'); } public function testLaunchDeploy(): void { - $this->PrepareCompileAuthent(); + $this->testLaunchCompile(); HubController::GetInstance()->LaunchDeploy(); $this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}'); } @@ -78,6 +72,7 @@ class HubControllerTest extends ItopDataTestCase $this->aFileToClean[] = $sExtensionDir; SetupUtils::rrmdir($sExtensionDir); + @mkdir($sExtensionDir); SetupUtils::copydir($sExtensionDir, $sProdModules); } } From f2e682c07c31eaa96f696a4424d60e94eda77a59 Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 30 Dec 2025 16:39:25 +0100 Subject: [PATCH 06/16] test SDK enhancement: be able to compare moduleinstallation ci: add log and hopefully fix hub test ci: fix all test that rely on CustomDataTC --- .../src/Service/DBToolsUtils.php | 44 +++++++++++++++++++ .../itop-hub-connector/HubControllerTest.php | 2 + .../src/BaseTestCase/ItopDataTestCase.php | 29 ++++++++++++ 3 files changed, 75 insertions(+) diff --git a/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php b/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php index 09dcf4824f..5abf29419a 100644 --- a/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php +++ b/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php @@ -10,6 +10,7 @@ namespace Combodo\iTop\DBTools\Service; use CMDBSource; use DBObjectSearch; use DBObjectSet; +use IssueLog; class DBToolsUtils { @@ -138,6 +139,49 @@ EOF; return $aValues; } + /** + * Return previous module installation. offset is applied on parent_id. + * @param $iOffset + * @return array + */ + public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array + { + $oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"'); + $oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first + $oSet->SetLimit($iOffset + 1); + + $iParentId = 0; + /** @var \DBObject $oModuleInstallation */ + while ($oModuleInstallation = $oSet->Fetch()) { + if ($iOffset == 0) { + $iParentId = $oModuleInstallation->Get('id'); + break; + } + $iOffset--; + } + + if ($iParentId === 0) { + IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]); + throw new \Exception("no ITOP_APPLICATION ModuleInstallation found"); + } + + $oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId"); + $oSet = new DBObjectSet($oFilter); // Most recent first + $aRawValues = $oSet->ToArrayOfValues(); + $aValues = []; + foreach ($aRawValues as $aRawValue) { + $aValue = []; + foreach ($aRawValue as $sAliasAttCode => $sValue) { + // remove 'mi.' from AttCode + $sAttCode = substr($sAliasAttCode, 3); + $aValue[$sAttCode] = $sValue; + } + + $aValues[] = $aValue; + } + return $aValues; + } + public static function GetDBTablesInfo() { self::AnalyzeTables(); diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php index fc34a62082..d14180fc5b 100644 --- a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php @@ -2,6 +2,7 @@ namespace Combodo\iTop\Test\UnitTest\HubConnector; +use Combodo\iTop\DBTools\Service\DBToolsUtils; use Combodo\iTop\HubConnector\Controller\HubController; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use DOMFormatException; @@ -45,6 +46,7 @@ class HubControllerTest extends ItopDataTestCase $this->testLaunchCompile(); HubController::GetInstance()->LaunchDeploy(); $this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}'); + $this->CompareCurrentAndPreviousModuleInstallations(); } private function CheckReport($sExpected) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index f2086f8bbe..ee00f7c46b 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest; use ArchivedObjectException; use CMDBObject; use CMDBSource; +use Combodo\iTop\DBTools\Service\DBToolsUtils; use Combodo\iTop\Service\Events\EventService; use Config; use Contact; @@ -1556,4 +1557,32 @@ abstract class ItopDataTestCase extends ItopTestCase @chmod($sConfigPath, 0440); @unlink($this->sConfigTmpBackupFile); } + + public function CompareCurrentAndPreviousModuleInstallations() + { + $this->RequireOnceItopFile('env-production/combodo-db-tools/src/Service/DBToolsUtils.php'); + $aPreviousInstallations = DBToolsUtils::GetPreviousModuleInstallationsByOffset(1); + $aInstallations = DBToolsUtils::GetPreviousModuleInstallationsByOffset(); + $this->assertEquals($this->KeepModuleInstallationComparableFields($aPreviousInstallations), $this->KeepModuleInstallationComparableFields($aInstallations)); + } + + public function KeepModuleInstallationComparableFields($aInstallations): array + { + $aRes = []; + $aIgnoredFields = ['id', 'parent_id', 'installed', 'comment']; + foreach ($aInstallations as $aData) { + $aNewData = []; + foreach ($aData as $sKey => $val) { + if (in_array($sKey, $aIgnoredFields)) { + continue; + } + $aNewData[$sKey] = $val; + } + $sName = $aNewData['name']; + $aRes[$sName] = $aNewData; + } + + asort($aRes); + return $aRes; + } } From 8ad4670a2f789b3fb97218294b8b2aa59f61017f Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 16:52:23 +0100 Subject: [PATCH 07/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20cleanup?= =?UTF-8?q?=20config=20use=20in=20setup=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/config.class.inc.php | 14 ++++---- setup/applicationinstaller.class.inc.php | 44 +++++------------------- setup/setuputils.class.inc.php | 4 +-- 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 82d8a786ef..970b7e3b7a 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -2685,14 +2685,13 @@ class Config * * @param array $aParamValues * @param ?string $sModulesDir - * @param bool $bPreserveModuleSettings * * @return void The current object is modified directly * * @throws \Exception * @throws \CoreException */ - public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false) + public function UpdateFromParams($aParamValues, $sModulesDir = null) { if (isset($aParamValues['application_path'])) { $this->Set('app_root_url', $aParamValues['application_path']); @@ -2740,7 +2739,10 @@ class Config } else { $aSelectedModules = null; } - $this->UpdateIncludes($sModulesDir, $aSelectedModules); + + if (! is_null($sModulesDir)) { + $this->UpdateIncludes($sModulesDir, $aSelectedModules); + } if (isset($aParamValues['source_dir'])) { $this->Set('source_dir', $aParamValues['source_dir']); @@ -2758,12 +2760,8 @@ class Config * * @throws Exception */ - public function UpdateIncludes($sModulesDir, $aSelectedModules = null) + public function UpdateIncludes(string $sModulesDir, $aSelectedModules = null) { - if ($sModulesDir === null) { - return; - } - // Initialize the arrays below with default values for the application... $oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values $aAddOns = $oEmptyConfig->GetAddOns(); diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index bb9b9de627..583da24765 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -46,6 +46,8 @@ class ApplicationInstaller protected $oParams; protected static $bMetaModelStarted = false; + protected Config $oConfig; + /** * @param \Parameters $oParams * @@ -57,9 +59,9 @@ class ApplicationInstaller $this->oParams = $oParams; $aParamValues = $oParams->GetParamForConfigArray(); - $oConfig = new Config(); - $oConfig->UpdateFromParams($aParamValues, null); - utils::SetConfig($oConfig); + $this->oConfig = new Config(); + $this->oConfig->UpdateFromParams($aParamValues); + utils::SetConfig($this->oConfig); } /** @@ -238,11 +240,8 @@ class ApplicationInstaller // __DB__-%Y-%m-%d $sDestination = $aPreinstall['backup']['destination']; $sSourceConfigFile = $aPreinstall['backup']['configuration_file']; - $aDBParams = $this->oParams->GetParamForConfigArray(); - $oTempConfig = new Config(); - $oTempConfig->UpdateFromParams($aDBParams); $sMySQLBinDir = $this->oParams->Get('mysql_bindir', null); - self::DoBackup($oTempConfig, $sDestination, $sSourceConfigFile, $sMySQLBinDir); + self::DoBackup($this->oConfig, $sDestination, $sSourceConfigFile, $sMySQLBinDir); $aResult = [ 'status' => self::OK, @@ -565,15 +564,9 @@ class ApplicationInstaller $sConfigFilePath = utils::GetConfigFilePath($sEnvironment); if (is_file($sConfigFilePath)) { $oConfig = new Config($sConfigFilePath); - } else { - $oConfig = null; - } - - if (false === is_null($oConfig)) { $oConfig->UpdateFromParams($aParamValues); + SetupUtils::EnterMaintenanceMode($oConfig); } - - SetupUtils::EnterMaintenanceMode($oConfig); } if (!is_dir($sTargetPath)) { @@ -682,11 +675,6 @@ class ApplicationInstaller $oConfig = new Config(); $oConfig->UpdateFromParams($aParamValues, $sModulesDir); - if ($bOldAddon) { - // Old version of the add-on for backward compatibility with pre-2.0 data models - $oConfig->SetAddons([]); - } - $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); $oProductionEnv->InitDataModel($oConfig, true); // load data model only @@ -860,11 +848,6 @@ class ApplicationInstaller $oConfig = new Config(); $oConfig->UpdateFromParams($aParamValues, $sModulesDir); - if ($bOldAddon) { - // Old version of the add-on for backward compatibility with pre-2.0 data models - $oConfig->SetAddons([]); - } - $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database $oContextTag = new ContextTag(ContextTag::TAG_SETUP); @@ -922,11 +905,6 @@ class ApplicationInstaller $oConfig = new Config(); $oConfig->UpdateFromParams($aParamValues, $sModulesDir); - if ($bOldAddon) { - // Old version of the add-on for backward compatibility with pre-2.0 data models - $oConfig->SetAddons([]); - } - $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); //Load the MetaModel if needed (asynchronous mode) @@ -980,12 +958,10 @@ class ApplicationInstaller $aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes); $sMode = $aParamValues['mode']; - $bPreserveModuleSettings = false; if ($sMode == 'upgrade') { try { $oOldConfig = new Config($sPreviousConfigFile); $oConfig = clone($oOldConfig); - $bPreserveModuleSettings = true; } catch (Exception $e) { // In case the previous configuration is corrupted... start with a blank new one $oConfig = new Config(); @@ -999,11 +975,7 @@ class ApplicationInstaller $oConfig->Set('access_mode', ACCESS_FULL); // Final config update: add the modules - $oConfig->UpdateFromParams($aParamValues, $sModulesDir, $bPreserveModuleSettings); - if ($bOldAddon) { - // Old version of the add-on for backward compatibility with pre-2.0 data models - $oConfig->SetAddons([]); - } + $oConfig->UpdateFromParams($aParamValues, $sModulesDir); // Record which modules are installed... $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 95ae569eb1..b46e67fb32 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1570,7 +1570,7 @@ JS $aParamValues = $oWizard->GetParamForConfigArray(); $aParamValues['source_dir'] = $sRelativeSourceDir; - $oConfig->UpdateFromParams($aParamValues, null); + $oConfig->UpdateFromParams($aParamValues); return $oConfig; } @@ -1627,7 +1627,7 @@ JS $aParamValues = $oWizard->GetParamForConfigArray(); $aParamValues['source_dir'] = ''; - $oConfig->UpdateFromParams($aParamValues, null); + $oConfig->UpdateFromParams($aParamValues); $oProductionEnv = new RunTimeEnvironment(); return $oProductionEnv->GetApplicationVersion($oConfig); From 27b0f643283f81017e5745e3efab181125619afb Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 17:18:00 +0100 Subject: [PATCH 08/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20cleanup?= =?UTF-8?q?=20main=20actions=20+=20remove=20static=20when=20possible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/applicationinstaller.class.inc.php | 86 +++++++++--------------- 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 583da24765..2e59874ed7 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -241,7 +241,7 @@ class ApplicationInstaller $sDestination = $aPreinstall['backup']['destination']; $sSourceConfigFile = $aPreinstall['backup']['configuration_file']; $sMySQLBinDir = $this->oParams->Get('mysql_bindir', null); - self::DoBackup($this->oConfig, $sDestination, $sSourceConfigFile, $sMySQLBinDir); + $this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir); $aResult = [ 'status' => self::OK, @@ -256,8 +256,6 @@ class ApplicationInstaller $aSelectedModules = $this->oParams->Get('selected_modules'); $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest'); $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions'); - $sTargetEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); $aMiscOptions = $this->oParams->Get('options', []); $bUseSymbolicLinks = null; @@ -271,12 +269,10 @@ class ApplicationInstaller } $aParamValues = $this->oParams->GetParamForConfigArray(); - self::DoCompile( + $this->DoCompile( $aSelectedModules, $sSourceDir, $sExtensionDir, - $sTargetDir, - $sTargetEnvironment, $bUseSymbolicLinks, $aParamValues ); @@ -292,17 +288,13 @@ class ApplicationInstaller case 'db-schema': $aSelectedModules = $this->oParams->Get('selected_modules', []); - $sTargetEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); $aParamValues = $this->oParams->GetParamForConfigArray(); $bOldAddon = $this->oParams->Get('old_addon', false); $sUrl = $this->oParams->Get('url', ''); - self::DoUpdateDBSchema( + $this->DoUpdateDBSchema( $aSelectedModules, - $sTargetDir, $aParamValues, - $sTargetEnvironment, $bOldAddon, $sUrl ); @@ -317,25 +309,19 @@ class ApplicationInstaller break; case 'after-db-create': - $sTargetEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); $aParamValues = $this->oParams->GetParamForConfigArray(); $aAdminParams = $this->oParams->Get('admin_account'); $sAdminUser = $aAdminParams['user']; $sAdminPwd = $aAdminParams['pwd']; $sAdminLanguage = $aAdminParams['language']; $aSelectedModules = $this->oParams->Get('selected_modules', []); - $bOldAddon = $this->oParams->Get('old_addon', false); - self::AfterDBCreate( - $sTargetDir, + $this->AfterDBCreate( $aParamValues, $sAdminUser, $sAdminPwd, $sAdminLanguage, - $aSelectedModules, - $sTargetEnvironment, - $bOldAddon + $aSelectedModules ); $aResult = [ @@ -349,18 +335,12 @@ class ApplicationInstaller case 'load-data': $aSelectedModules = $this->oParams->Get('selected_modules'); - $sTargetEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); $aParamValues = $this->oParams->GetParamForConfigArray(); - $bOldAddon = $this->oParams->Get('old_addon', false); $bSampleData = ($this->oParams->Get('sample_data', 0) == 1); - self::DoLoadFiles( + $this->DoLoadFiles( $aSelectedModules, - $sTargetDir, $aParamValues, - $sTargetEnvironment, - $bOldAddon, $bSampleData ); @@ -374,21 +354,15 @@ class ApplicationInstaller break; case 'create-config': - $sTargetEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); $sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', ''); $sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0'); - $bOldAddon = $this->oParams->Get('old_addon', false); $aSelectedModuleCodes = $this->oParams->Get('selected_modules', []); $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []); $aParamValues = $this->oParams->GetParamForConfigArray(); - self::DoCreateConfig( - $sTargetDir, + $this->DoCreateConfig( $sPreviousConfigFile, - $sTargetEnvironment, $sDataModelVersion, - $bOldAddon, $aSelectedModuleCodes, $aSelectedExtensionCodes, $aParamValues, @@ -493,7 +467,6 @@ class ApplicationInstaller } /** - * @param Config $oConfig * @param string $sBackupFileFormat * @param string $sSourceConfigFile * @param string $sMySQLBinDir @@ -503,15 +476,15 @@ class ApplicationInstaller * @throws \MySQLException * @since 2.5.0 uses a {@link Config} object to store DB parameters */ - protected static function DoBackup($oConfig, $sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null) + protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null) { - $oBackup = new SetupDBBackup($oConfig); + $oBackup = new SetupDBBackup($this->oConfig); $sTargetFile = $oBackup->MakeName($sBackupFileFormat); if (!empty($sMySQLBinDir)) { $oBackup->SetMySQLBinDir($sMySQLBinDir); } - CMDBSource::InitFromConfig($oConfig); + CMDBSource::InitFromConfig($this->oConfig); $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); } @@ -530,7 +503,7 @@ class ApplicationInstaller * * @since 3.1.0 N°2013 added the aParamValues param */ - protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sEnvironment, $bUseSymbolicLinks = null, $aParamValues = []) + protected function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null, $aParamValues = []) { SetupLog::Info("Compiling data model."); @@ -538,6 +511,9 @@ class ApplicationInstaller require_once(APPROOT.'setup/modelfactory.class.inc.php'); require_once(APPROOT.'setup/compiler.class.inc.php'); + $sEnvironment = $this->GetTargetEnv(); + $sTargetDir = $this->GetTargetDir(); + if (empty($sSourceDir) || empty($sTargetDir)) { throw new Exception("missing parameter source_dir and/or target_dir"); } @@ -660,8 +636,11 @@ class ApplicationInstaller * @throws \CoreException * @throws \MySQLException */ - protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aParamValues, $sTargetEnvironment = '', $bOldAddon = false, $sAppRootUrl = '') + protected function DoUpdateDBSchema($aSelectedModules, $aParamValues, $bOldAddon = false, $sAppRootUrl = '') { + $sTargetEnvironment = $this->GetTargetEnv(); + $sModulesDir = $this->GetTargetDir(); + /** * @since 3.2.0 move the ContextTag init at the very beginning of the method * @noinspection PhpUnusedLocalVariableInspection @@ -827,16 +806,16 @@ class ApplicationInstaller ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields'); } - protected static function AfterDBCreate( - $sModulesDir, + protected function AfterDBCreate( $aParamValues, $sAdminUser, $sAdminPwd, $sAdminLanguage, - $aSelectedModules, - $sTargetEnvironment, - $bOldAddon + $aSelectedModules ) { + $sTargetEnvironment = $this->GetTargetEnv(); + $sModulesDir = $this->GetTargetDir(); + /** * @since 3.2.0 move the ContextTag init at the very beginning of the method * @noinspection PhpUnusedLocalVariableInspection @@ -888,14 +867,14 @@ class ApplicationInstaller } } - protected static function DoLoadFiles( + protected function DoLoadFiles( $aSelectedModules, - $sModulesDir, $aParamValues, - $sTargetEnvironment = 'production', - $bOldAddon = false, $bSampleData = false ) { + $sTargetEnvironment = $this->GetTargetEnv(); + $sModulesDir = $this->GetTargetDir(); + /** * @since 3.2.0 move the ContextTag init at the very beginning of the method * @noinspection PhpUnusedLocalVariableInspection @@ -923,11 +902,8 @@ class ApplicationInstaller } /** - * @param string $sModulesDir * @param string $sPreviousConfigFile - * @param string $sTargetEnvironment * @param string $sDataModelVersion - * @param boolean $bOldAddon * @param array $aSelectedModuleCodes * @param array $aSelectedExtensionCodes * @param array $aParamValues parameters array used to create config file using {@see Config::UpdateFromParams} @@ -938,17 +914,17 @@ class ApplicationInstaller * @throws \CoreException * @throws \Exception */ - protected static function DoCreateConfig( - $sModulesDir, + protected function DoCreateConfig( $sPreviousConfigFile, - $sTargetEnvironment, $sDataModelVersion, - $bOldAddon, $aSelectedModuleCodes, $aSelectedExtensionCodes, $aParamValues, $sInstallComment = null ) { + $sTargetEnvironment = $this->GetTargetEnv(); + $sModulesDir = $this->GetTargetDir(); + /** * @since 3.2.0 move the ContextTag init at the very beginning of the method * @noinspection PhpUnusedLocalVariableInspection From 774fe22ece0d82163a705a1cea16a23266a04340 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 17:18:39 +0100 Subject: [PATCH 09/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20cache=20G?= =?UTF-8?q?etParamForConfigArray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/parameters.class.inc.php | 37 ++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/setup/parameters.class.inc.php b/setup/parameters.class.inc.php index 40680a2da8..bb22e2889b 100644 --- a/setup/parameters.class.inc.php +++ b/setup/parameters.class.inc.php @@ -7,6 +7,7 @@ class InvalidParameterException extends Exception abstract class Parameters { public $aData = null; + private ?array $aParamValues = null; public function __construct() { @@ -26,24 +27,26 @@ abstract class Parameters */ public function GetParamForConfigArray() { - $aDBParams = $this->Get('database'); - $aParamValues = [ - 'mode' => $this->Get('mode'), - 'db_server' => $aDBParams['server'], - 'db_user' => $aDBParams['user'], - 'db_pwd' => $aDBParams['pwd'], - 'db_name' => $aDBParams['name'], - 'new_db_name' => $aDBParams['name'], - 'db_prefix' => $aDBParams['prefix'], - 'db_tls_enabled' => $aDBParams['db_tls_enabled'], - 'db_tls_ca' => $aDBParams['db_tls_ca'], - 'application_path' => $this->Get('url', ''), - 'language' => $this->Get('language', ''), - 'graphviz_path' => $this->Get('graphviz_path', ''), - 'source_dir' => $this->Get('source_dir', ''), - ]; + if (is_null($this->aParamValues)) { + $aDBParams = $this->Get('database'); + $this->aParamValues = [ + 'mode' => $this->Get('mode'), + 'db_server' => $aDBParams['server'], + 'db_user' => $aDBParams['user'], + 'db_pwd' => $aDBParams['pwd'], + 'db_name' => $aDBParams['name'], + 'new_db_name' => $aDBParams['name'], + 'db_prefix' => $aDBParams['prefix'], + 'db_tls_enabled' => $aDBParams['db_tls_enabled'], + 'db_tls_ca' => $aDBParams['db_tls_ca'], + 'application_path' => $this->Get('url', ''), + 'language' => $this->Get('language', ''), + 'graphviz_path' => $this->Get('graphviz_path', ''), + 'source_dir' => $this->Get('source_dir', ''), + ]; + } - return $aParamValues; + return $this->aParamValues; } public function Set($sCode, $value) From fdfe9224c35ab3df171f9d5035a477612e33248d Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 17:18:55 +0100 Subject: [PATCH 10/16] =?UTF-8?q?N=C2=B08981=20:=20add=20type=20in=20funct?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/setuputils.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index b46e67fb32..017d90bc05 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1555,7 +1555,7 @@ JS return $sHtml; } - public static function GetConfig($oWizard) + public static function GetConfig(WizardController $oWizard) { $oConfig = new Config(); $sSourceDir = $oWizard->GetParameter('source_dir', ''); From 193c980057acf4aeb6ced0e619c1897a70817603 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 17:23:19 +0100 Subject: [PATCH 11/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20static=20?= =?UTF-8?q?removal=20+=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/applicationinstaller.class.inc.php | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 2e59874ed7..8602f7dad2 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -217,7 +217,7 @@ class ApplicationInstaller $aPreinstall = $this->oParams->Get('preinstall'); $aCopies = $aPreinstall['copies'] ?? []; - self::DoCopy($aCopies); + $this->DoCopy($aCopies); $sReport = "Copying..."; $aResult = [ @@ -268,13 +268,11 @@ class ApplicationInstaller } } - $aParamValues = $this->oParams->GetParamForConfigArray(); $this->DoCompile( $aSelectedModules, $sSourceDir, $sExtensionDir, - $bUseSymbolicLinks, - $aParamValues + $bUseSymbolicLinks ); $aResult = [ @@ -309,7 +307,6 @@ class ApplicationInstaller break; case 'after-db-create': - $aParamValues = $this->oParams->GetParamForConfigArray(); $aAdminParams = $this->oParams->Get('admin_account'); $sAdminUser = $aAdminParams['user']; $sAdminPwd = $aAdminParams['pwd']; @@ -317,7 +314,6 @@ class ApplicationInstaller $aSelectedModules = $this->oParams->Get('selected_modules', []); $this->AfterDBCreate( - $aParamValues, $sAdminUser, $sAdminPwd, $sAdminLanguage, @@ -335,12 +331,10 @@ class ApplicationInstaller case 'load-data': $aSelectedModules = $this->oParams->Get('selected_modules'); - $aParamValues = $this->oParams->GetParamForConfigArray(); $bSampleData = ($this->oParams->Get('sample_data', 0) == 1); $this->DoLoadFiles( $aSelectedModules, - $aParamValues, $bSampleData ); @@ -358,14 +352,12 @@ class ApplicationInstaller $sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0'); $aSelectedModuleCodes = $this->oParams->Get('selected_modules', []); $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []); - $aParamValues = $this->oParams->GetParamForConfigArray(); $this->DoCreateConfig( $sPreviousConfigFile, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, - $aParamValues, $sInstallComment ); @@ -446,7 +438,7 @@ class ApplicationInstaller SetupUtils::ExitReadOnlyMode(); } - protected static function DoCopy($aCopies) + protected function DoCopy($aCopies) { $aReports = []; foreach ($aCopies as $aCopy) { @@ -492,10 +484,7 @@ class ApplicationInstaller * @param array $aSelectedModules * @param string $sSourceDir * @param string $sExtensionDir - * @param string $sTargetDir - * @param string $sEnvironment * @param boolean $bUseSymbolicLinks - * @param array $aParamValues * * @return void * @throws \ConfigException @@ -503,7 +492,7 @@ class ApplicationInstaller * * @since 3.1.0 N°2013 added the aParamValues param */ - protected function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null, $aParamValues = []) + protected function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null) { SetupLog::Info("Compiling data model."); @@ -511,6 +500,7 @@ class ApplicationInstaller require_once(APPROOT.'setup/modelfactory.class.inc.php'); require_once(APPROOT.'setup/compiler.class.inc.php'); + $aParamValues = $this->oParams->GetParamForConfigArray(); $sEnvironment = $this->GetTargetEnv(); $sTargetDir = $this->GetTargetDir(); @@ -807,12 +797,12 @@ class ApplicationInstaller } protected function AfterDBCreate( - $aParamValues, $sAdminUser, $sAdminPwd, $sAdminLanguage, $aSelectedModules ) { + $aParamValues = $this->oParams->GetParamForConfigArray(); $sTargetEnvironment = $this->GetTargetEnv(); $sModulesDir = $this->GetTargetDir(); @@ -869,9 +859,9 @@ class ApplicationInstaller protected function DoLoadFiles( $aSelectedModules, - $aParamValues, $bSampleData = false ) { + $aParamValues = $this->oParams->GetParamForConfigArray(); $sTargetEnvironment = $this->GetTargetEnv(); $sModulesDir = $this->GetTargetDir(); @@ -919,9 +909,9 @@ class ApplicationInstaller $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, - $aParamValues, $sInstallComment = null ) { + $aParamValues = $this->oParams->GetParamForConfigArray(); $sTargetEnvironment = $this->GetTargetEnv(); $sModulesDir = $this->GetTargetDir(); From 9f3b8ec964d82ff4e7ad3fc517092016413a6d3f Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 21:13:39 +0100 Subject: [PATCH 12/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20optimize?= =?UTF-8?q?=20IsConnectableToITopHub=20use=20(perf)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N°8981: revert IsConnectableToITopHub --- setup/AnalyzeInstallation.php | 1 + setup/wizardsteps.class.inc.php | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/setup/AnalyzeInstallation.php b/setup/AnalyzeInstallation.php index 96b902ce50..ca0c35ea1a 100644 --- a/setup/AnalyzeInstallation.php +++ b/setup/AnalyzeInstallation.php @@ -58,6 +58,7 @@ class AnalyzeInstallation * ) * @throws \Exception */ + public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null) { $aRes = [ diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index c2f9860f47..67b2a18aa9 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -672,9 +672,13 @@ class WizStepLicense extends WizardStep private function NeedsGdprConsent() { $sMode = $this->oWizard->GetParameter('install_mode'); - $aModules = SetupUtils::AnalyzeInstallation($this->oWizard); - return (($sMode === 'install') && SetupUtils::IsConnectableToITopHub($aModules)); + if ($sMode !== 'install') { + return false; + } + + $aModules = SetupUtils::AnalyzeInstallation($this->oWizard); + return SetupUtils::IsConnectableToITopHub($aModules); } /** From 57b36101008e8fb9448d43963bdf6f68e8df0f38 Mon Sep 17 00:00:00 2001 From: odain Date: Mon, 5 Jan 2026 22:17:23 +0100 Subject: [PATCH 13/16] =?UTF-8?q?N=C2=B08981=20setup=20wizard:=20reuse=20A?= =?UTF-8?q?nalyzeInstallation=20result=20in=20wizard=20module=20choice=20s?= =?UTF-8?q?tep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/applicationinstaller.class.inc.php | 1 - setup/wizardsteps.class.inc.php | 27 +++++++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index 8602f7dad2..f658b130df 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -698,7 +698,6 @@ class ApplicationInstaller } // Module specific actions (migrate the data) - // $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules); diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 67b2a18aa9..1ba2cc218e 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1334,6 +1334,9 @@ class WizStepModulesChoice extends WizardStep */ protected bool $bChoicesFromDatabase; + private array $aAnalyzeInstallationModules; + private ?MissingDependencyException $oMissingDependencyException = null; + public function __construct(WizardController $oWizard, $sCurrentState) { parent::__construct($oWizard, $sCurrentState); @@ -1358,6 +1361,14 @@ class WizStepModulesChoice extends WizardStep $this->oExtensionsMap->LoadChoicesFromDatabase($this->oConfig); $this->bChoicesFromDatabase = true; } + + // Sanity check (not stopper, to let developers go further...) + try { + $this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard, true); + } catch (MissingDependencyException $e) { + $this->oMissingDependencyException = $e; + $this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard); + } } public function GetTitle() @@ -1449,10 +1460,8 @@ class WizStepModulesChoice extends WizardStep protected function DisplayStep($oPage) { // Sanity check (not stopper, to let developers go further...) - try { - SetupUtils::AnalyzeInstallation($this->oWizard, true); - } catch (MissingDependencyException $e) { - $oPage->warning($e->getHtmlDesc(), $e->getMessage()); + if (! is_null($this->oMissingDependencyException)) { + $oPage->warning($this->oMissingDependencyException->getHtmlDesc(), $this->oMissingDependencyException->getMessage()); } $this->bUpgrade = ($this->oWizard->GetParameter('install_mode') != 'install'); @@ -1463,9 +1472,8 @@ class WizStepModulesChoice extends WizardStep $oPage->add_style(".choice-disabled { color: #999; }"); $oPage->add_style("input.unremovable { accent-color: orangered;}"); - $aModules = SetupUtils::AnalyzeInstallation($this->oWizard); $sManualInstallError = SetupUtils::CheckManualInstallDirEmpty( - $aModules, + $this->aAnalyzeInstallationModules, $this->oWizard->GetParameter('extensions_dir', 'extensions') ); if ($sManualInstallError !== '') { @@ -1491,7 +1499,7 @@ class WizStepModulesChoice extends WizardStep $oPage->add(''); // Build the default choices - $aDefaults = $this->GetDefaults($aStepInfo, $aModules); + $aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules); $index = $this->GetStepIndex(); // retrieve the saved selection @@ -1751,7 +1759,7 @@ EOF { if ($sParentId == '') { // Check once (before recursing) that the hidden modules are selected - foreach (SetupUtils::AnalyzeInstallation($this->oWizard) as $sModuleId => $aModule) { + foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) { if (($sModuleId != ROOT_MODULE) && !isset($aModules[$sModuleId])) { if (($aModule['category'] == 'authentication') || (!$aModule['visible'] && !isset($aModule['auto_select']))) { $aModules[$sModuleId] = true; @@ -1841,11 +1849,10 @@ EOF if ($sParentId == '') { // Last pass (after all the user's choices are turned into "selected" modules): // Process 'auto_select' modules for modules that are not already selected - $aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard); do { // Loop while new modules are added... $bModuleAdded = false; - foreach ($aAvailableModules as $sModuleId => $aModule) { + foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) { if (($sModuleId != ROOT_MODULE) && !array_key_exists($sModuleId, $aModules) && isset($aModule['auto_select'])) { try { SetupInfo::SetSelectedModules($aModules); From 5f2604c610a937e744c7225f73e11223b96b2f0e Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 6 Jan 2026 17:06:24 +0100 Subject: [PATCH 14/16] =?UTF-8?q?N=C2=B08981:=20be=20able=20to=20remove=20?= =?UTF-8?q?extension=20during=20setup=20even=20when=20present=20on=20disk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/applicationinstaller.class.inc.php | 11 +- setup/extensionsmap.class.inc.php | 169 +++--------------- .../DryRemovalRuntimeEnvironment.php | 42 +---- setup/itopextension.class.inc.php | 143 +++++++++++++++ setup/modulediscovery.class.inc.php | 66 ++++++- setup/modulediscovery/ModuleFileReader.php | 5 +- setup/setuputils.class.inc.php | 7 + setup/wizardsteps.class.inc.php | 4 +- 8 files changed, 252 insertions(+), 195 deletions(-) create mode 100644 setup/itopextension.class.inc.php diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index f658b130df..d348597a74 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -257,6 +257,10 @@ class ApplicationInstaller $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest'); $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions'); $aMiscOptions = $this->oParams->Get('options', []); + $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', null); + if (! is_array($aRemovedExtensionCodes)) { + $aRemovedExtensionCodes = []; + } $bUseSymbolicLinks = null; if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) { @@ -269,6 +273,7 @@ class ApplicationInstaller } $this->DoCompile( + $aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, @@ -481,6 +486,7 @@ class ApplicationInstaller } /** + * @param array $aRemovedExtensionCodes * @param array $aSelectedModules * @param string $sSourceDir * @param string $sExtensionDir @@ -492,7 +498,7 @@ class ApplicationInstaller * * @since 3.1.0 N°2013 added the aParamValues param */ - protected function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null) + protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null) { SetupLog::Info("Compiling data model."); @@ -548,6 +554,9 @@ class ApplicationInstaller SetupUtils::tidydir($sTargetPath); } + $oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan); + $oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes); + $oFactory = new ModelFactory($aDirsToScan); $oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries'); diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index aac0aa753c..5785d25122 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -3,143 +3,11 @@ use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; +require_once(APPROOT.'/setup/itopextension.class.inc.php'); require_once(APPROOT.'/setup/parameters.class.inc.php'); require_once(APPROOT.'/core/cmdbsource.class.inc.php'); require_once(APPROOT.'/setup/modulediscovery.class.inc.php'); require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); -/** - * Basic helper class to describe an extension, with some characteristics and a list of modules - */ -class iTopExtension -{ - public const SOURCE_WIZARD = 'datamodels'; - public const SOURCE_MANUAL = 'extensions'; - public const SOURCE_REMOTE = 'data'; - - /** - * @var string - */ - public $sCode; - - /** - * @var string - */ - public $sVersion; - - /** - * @var string - */ - public $sInstalledVersion; - - /** - * @var string - */ - public $sLabel; - - /** - * @var string - */ - public $sDescription; - - /** - * @var string - */ - public $sSource; - - /** - * @var bool - */ - public $bMandatory; - - /** - * @var string - */ - public $sMoreInfoUrl; - - /** - * @var bool - */ - public $bMarkedAsChosen; - /** - * If null, check if at least one module cannot be uninstalled - * @var bool|null - */ - public ?bool $bCanBeUninstalled = null; - - /** - * @var bool - */ - public $bVisible; - - /** - * @var string[] - */ - public $aModules; - - /** - * @var string[] - */ - public $aModuleVersion; - - /** - * @var string[] - */ - public $aModuleInfo; - - /** - * @var string - */ - public $sSourceDir; - - /** - * - * @var string[] - */ - public $aMissingDependencies; - /** - * @var bool - */ - public bool $bInstalled = false; - /** - * @var bool - */ - public bool $bRemovedFromDisk = false; - - public function __construct() - { - $this->sCode = ''; - $this->sLabel = ''; - $this->sDescription = ''; - $this->sSource = self::SOURCE_WIZARD; - $this->bMandatory = false; - $this->sMoreInfoUrl = ''; - $this->bMarkedAsChosen = false; - $this->sVersion = ITOP_VERSION; - $this->sInstalledVersion = ''; - $this->aModules = []; - $this->aModuleVersion = []; - $this->aModuleInfo = []; - $this->sSourceDir = ''; - $this->bVisible = true; - $this->aMissingDependencies = []; - } - - /** - * @since 3.3.0 - * @return bool - */ - public function CanBeUninstalled(): bool - { - if (!is_null($this->bCanBeUninstalled)) { - return $this->bCanBeUninstalled; - } - foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) { - $this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes'; - return $this->bCanBeUninstalled; - } - return true; - } -} /** * Helper class to discover all available extensions on a given iTop system @@ -308,28 +176,31 @@ class iTopExtensionsMap return $this->aExtensionsByCode[$sExtensionCode] ?? null; } - /*public function GetMissingExtensions(array $aSelectedExtensions) + /** + * @param array $aExtensionCodes + * @return void + */ + public function DeclareExtensionAsRemoved(array $aExtensionCodes): void { - \SetupLog::Info(__METHOD__, null, ['selected' => $aSelectedExtensions]); - $aExtensionsFromDb = array_keys($this->aExtensionsByCode); - sort($aExtensionsFromDb); - \SetupLog::Info(__METHOD__, null, ['found' => $aExtensionsFromDb]); + if (count($aExtensionCodes) === 0) { + \ModuleDiscovery::DeclareRemovedExtensions([]); + return; + } - $aRes = []; - foreach (array_diff($aExtensionsFromDb, $aSelectedExtensions) as $sExtensionCode) { - $oExtension = $this->GetFromExtensionCode($sExtensionCode); - if (!is_null($oExtension) && $oExtension->bVisible && $oExtension->sSource != iTopExtension::SOURCE_WIZARD) { - - \SetupLog::Info(__METHOD__."$sExtensionCode", null, ['visible' => $oExtension->bVisible, 'mandatory' => $oExtension->bMandatory]); - $aRes [] = $sExtensionCode; + $aRemovedExtension = []; + foreach ($aExtensionCodes as $sCode) { + /** @var \iTopExtension $oExtension */ + $oExtension = $this->GetFromExtensionCode($sCode); + if (!is_null($oExtension)) { + $aRemovedExtension [] = $oExtension; + \IssueLog::Info(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]); } else { - \SetupLog::Info(__METHOD__." MISSING $sExtensionCode"); + \IssueLog::Warning(__METHOD__." cannot find extensions", null, ['code' => $sCode]); } } - \SetupLog::Info(__METHOD__, null, $aRes); - return $aRes; - }*/ + \ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension); + } /** * Read (recursively) a directory to find if it contains extensions (or modules) diff --git a/setup/feature_removal/DryRemovalRuntimeEnvironment.php b/setup/feature_removal/DryRemovalRuntimeEnvironment.php index d7f362db8f..94e318e175 100644 --- a/setup/feature_removal/DryRemovalRuntimeEnvironment.php +++ b/setup/feature_removal/DryRemovalRuntimeEnvironment.php @@ -2,6 +2,7 @@ namespace Combodo\iTop\Setup\FeatureRemoval; +use iTopExtensionsMap; use MetaModel; use RunTimeEnvironment; use SetupUtils; @@ -11,7 +12,6 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment public const DRY_REMOVAL_AUDIT_ENV = "extension-removal"; protected array $aExtensionsByCode; - private bool $bExtensionMapModified = false; /** * Toolset for building a run-time environment @@ -41,29 +41,16 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment $this->Cleanup(); SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules"); - if (count($aExtensionCodesToRemove) > 0) { - $this->RemoveExtensionsLocally($aExtensionCodesToRemove); - } + $this->DeclareExtensionAsRemoved($aExtensionCodesToRemove); $oDryRemovalConfig = clone(MetaModel::GetConfig()); $oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv); $this->WriteConfigFileSafe($oDryRemovalConfig); } - private function RemoveExtensionsLocally(array $aExtensionCodes): void + private function DeclareExtensionAsRemoved(array $aExtensionCodes): void { - $oExtensionsMap = new \iTopExtensionsMap($this->sFinalEnv); - - foreach ($aExtensionCodes as $sCode) { - /** @var \iTopExtension $oExtension */ - $oExtension = $oExtensionsMap->GetFromExtensionCode($sCode); - if (!is_null($oExtension)) { - $sDir = $oExtension->sSourceDir; - \IssueLog::Info(__METHOD__.": remove extension locally", null, [$oExtension->sCode => $sDir]); - SetupUtils::rrmdir($sDir); - } else { - \IssueLog::Warning(__METHOD__." cannot find extensions", null, ['env' => $this->sFinalEnv, 'code' => $sCode]); - } - } + $oExtensionsMap = new iTopExtensionsMap($this->sFinalEnv); + $oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes); } public function Cleanup() @@ -75,23 +62,4 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment SetupUtils::rrmdir(APPROOT."/conf/$sEnv"); @unlink(APPROOT."/data/datamodel-$sEnv.xml"); } - - /** - * @return \iTopExtensionsMap|null - */ - /*protected function GetExtensionMap(): ?iTopExtensionsMap - { - if (is_null(parent::GetExtensionMap())) { - return null; - } - - if (!$this->bExtensionMapModified) { - $this->bExtensionMapModified = true; - foreach ($this->aExtensionsByCode as $sCode) { - parent::GetExtensionMap()->RemoveExtension($sCode); - } - } - - return parent::GetExtensionMap(); - }*/ } diff --git a/setup/itopextension.class.inc.php b/setup/itopextension.class.inc.php new file mode 100644 index 0000000000..e7c8af96d7 --- /dev/null +++ b/setup/itopextension.class.inc.php @@ -0,0 +1,143 @@ +sCode = ''; + $this->sLabel = ''; + $this->sDescription = ''; + $this->sSource = self::SOURCE_WIZARD; + $this->bMandatory = false; + $this->sMoreInfoUrl = ''; + $this->bMarkedAsChosen = false; + $this->sVersion = ITOP_VERSION; + $this->sInstalledVersion = ''; + $this->aModules = []; + $this->aModuleVersion = []; + $this->aModuleInfo = []; + $this->sSourceDir = ''; + $this->bVisible = true; + $this->aMissingDependencies = []; + } + + /** + * @since 3.3.0 + * @return bool + */ + public function CanBeUninstalled(): bool + { + if (!is_null($this->bCanBeUninstalled)) { + return $this->bCanBeUninstalled; + } + foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) { + $this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes'; + return $this->bCanBeUninstalled; + } + return true; + } +} diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 6c334a90db..84058c2d18 100755 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -27,6 +27,7 @@ use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php'); require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php'); +require_once(__DIR__.'/itopextension.class.inc.php'); use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort; @@ -95,6 +96,9 @@ class ModuleDiscovery protected static $m_aModules = []; protected static $m_aModuleVersionByName = []; + /** @var array<\iTopExtension $m_aRemovedExtensions */ + protected static $m_aRemovedExtensions = []; + // All the entries below are list of file paths relative to the module directory protected static $m_aFilesList = ['datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample']; @@ -131,6 +135,10 @@ class ModuleDiscovery list($sModuleName, $sModuleVersion) = static::GetModuleName($sId); + if (self::IsModulePartOfRemovedExtension($sModuleName, $sModuleVersion, $aArgs)) { + return; + } + if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) { if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) { // Newer version, let's upgrade @@ -214,15 +222,20 @@ class ModuleDiscovery */ public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null) { - if (is_null($aModulesToLoad)) { + if (is_null($aModulesToLoad) && count(self::$m_aRemovedExtensions) === 0) { $aFilteredModules = $aModules; } else { $aFilteredModules = []; - foreach ($aModules as $sModuleId => $aModule) { + foreach ($aModules as $sModuleId => $aModuleInfo) { $oModule = new Module($sModuleId); $sModuleName = $oModule->GetModuleName(); - if (in_array($sModuleName, $aModulesToLoad)) { - $aFilteredModules[$sModuleId] = $aModule; + + if (self::IsModulePartOfRemovedExtension($sModuleName, $oModule->GetVersion(), $aModuleInfo)) { + continue; + } + + if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) { + $aFilteredModules[$sModuleId] = $aModuleInfo; } } } @@ -230,6 +243,51 @@ class ModuleDiscovery return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency); } + /** + * @param array<\iTopExtension> $aRemovedExtension + * @return void + */ + public static function DeclareRemovedExtensions(array $aRemovedExtension) + { + if (self::$m_aRemovedExtensions != $aRemovedExtension) { + self::ResetCache(); + } + SetupLog::Info(__METHOD__, null, ['count' => count($aRemovedExtension)]); + self::$m_aRemovedExtensions = $aRemovedExtension; + } + + private static function IsModulePartOfRemovedExtension(string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool + { + if (count(self::$m_aRemovedExtensions) === 0) { + return false; + } + + /** @var \iTopExtension $oExtension */ + foreach (self::$m_aRemovedExtensions as $oExtension) { + $sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null; + if (is_null($sCurrentVersion)) { + continue; + } + + if ($sModuleVersion !== $sCurrentVersion) { + continue; + } + + $aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null; + + $sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; + $sPath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; + if (realpath($sPath) !== realpath($sCurrentModuleFilePath)) { + continue; + } + + SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ]); + return true; + } + + return false; + } + private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator { if (!isset(static::$oPhpExpressionEvaluator)) { diff --git a/setup/modulediscovery/ModuleFileReader.php b/setup/modulediscovery/ModuleFileReader.php index 00cb7158ea..048388000f 100644 --- a/setup/modulediscovery/ModuleFileReader.php +++ b/setup/modulediscovery/ModuleFileReader.php @@ -36,6 +36,7 @@ class ModuleFileReader public const MODULE_INFO_PATH = 0; public const MODULE_INFO_ID = 1; public const MODULE_INFO_CONFIG = 2; + public const MODULE_FILE_PATH = "module_file_path"; public const STATIC_CALLWHITELIST = [ "utils::GetItopVersionWikiSyntax", @@ -164,7 +165,7 @@ class ModuleFileReader private function CompleteModuleInfoWithFilePath(array &$aModuleInfo) { if (count($aModuleInfo) == 3) { - $aModuleInfo[static::MODULE_INFO_CONFIG]['module_file_path'] = $aModuleInfo[static::MODULE_INFO_PATH]; + $aModuleInfo[static::MODULE_INFO_CONFIG][self::MODULE_FILE_PATH] = $aModuleInfo[static::MODULE_INFO_PATH]; } } @@ -180,7 +181,7 @@ class ModuleFileReader } if (!class_exists($sModuleInstallerClass)) { - $sModuleFilePath = $aModuleInfo['module_file_path']; + $sModuleFilePath = $aModuleInfo[self::MODULE_FILE_PATH]; $this->ReadModuleFileInformationUnsafe($sModuleFilePath); } diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 017d90bc05..cf77bdf79a 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1602,6 +1602,13 @@ JS $aDirsToScan[] = $sExtraDir; } $oProductionEnv = new RunTimeEnvironment(); + $aRemovedExtensionCodes = $oWizard->GetParameter('removed_extensions', null); + if (! is_array($aRemovedExtensionCodes)) { + $aRemovedExtensionCodes = []; + } + $oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan); + $oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes); + $aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad); foreach ($aAvailableModules as $key => $aModule) { diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index 1ba2cc218e..680e3a7479 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -1439,7 +1439,7 @@ class WizStepModulesChoice extends WizardStep $this->oWizard->SetParameter('selected_extensions', json_encode($aExtensions)); $this->oWizard->SetParameter('display_choices', $sDisplayChoices); $this->oWizard->SetParameter('extensions_added', json_encode($aExtensionsAdded)); - $this->oWizard->SetParameter('extensions_removed', json_encode($aExtensionsRemoved)); + $this->oWizard->SetParameter('removed_extensions', json_encode($aExtensionsRemoved)); $this->oWizard->SetParameter('extensions_not_uninstallable', json_encode(array_keys($aExtensionsNotUninstallable))); return ['class' => 'WizStepSummary', 'state' => '']; } @@ -2272,7 +2272,7 @@ class WizStepSummary extends WizardStep $oPage->add(''); $oPage->add('
Extensions to be uninstalled'); - $aExtensionsRemoved = json_decode($this->oWizard->GetParameter('extensions_removed'), true); + $aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true); $aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable')); $sExtensionsRemoved = ''; if (count($aExtensionsRemoved) > 0) { From e55bbf728b788622a99a7f2e0072a44b4098a2cf Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 7 Jan 2026 19:48:34 +0100 Subject: [PATCH 15/16] =?UTF-8?q?N=C2=B08981:=20review=20cleanup=20on=20mo?= =?UTF-8?q?dule=20filtering=20due=20to=20extensoin=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/applicationinstaller.class.inc.php | 5 +---- setup/extensionsmap.class.inc.php | 5 ----- setup/modulediscovery.class.inc.php | 24 ++++++++++++++++++------ setup/setuputils.class.inc.php | 5 +---- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index d348597a74..377998b05b 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -257,10 +257,7 @@ class ApplicationInstaller $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest'); $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions'); $aMiscOptions = $this->oParams->Get('options', []); - $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', null); - if (! is_array($aRemovedExtensionCodes)) { - $aRemovedExtensionCodes = []; - } + $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []); $bUseSymbolicLinks = null; if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) { diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 5785d25122..a541b7ac18 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -182,11 +182,6 @@ class iTopExtensionsMap */ public function DeclareExtensionAsRemoved(array $aExtensionCodes): void { - if (count($aExtensionCodes) === 0) { - \ModuleDiscovery::DeclareRemovedExtensions([]); - return; - } - $aRemovedExtension = []; foreach ($aExtensionCodes as $sCode) { /** @var \iTopExtension $oExtension */ diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 84058c2d18..e1c00e76bd 100755 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -252,7 +252,6 @@ class ModuleDiscovery if (self::$m_aRemovedExtensions != $aRemovedExtension) { self::ResetCache(); } - SetupLog::Info(__METHOD__, null, ['count' => count($aRemovedExtension)]); self::$m_aRemovedExtensions = $aRemovedExtension; } @@ -262,6 +261,9 @@ class ModuleDiscovery return false; } + $aNonMatchingPaths = []; + $sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; + /** @var \iTopExtension $oExtension */ foreach (self::$m_aRemovedExtensions as $oExtension) { $sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null; @@ -274,17 +276,27 @@ class ModuleDiscovery } $aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null; - - $sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; - $sPath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; - if (realpath($sPath) !== realpath($sCurrentModuleFilePath)) { + if (is_null($aCurrentModuleInfo)) { + SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen"); continue; } - SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ]); + // use case: same module coming from 2 different extensions + // we remove only the one coming from removed extensions + $sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; + if (realpath($sModuleFilePath) !== realpath($sCurrentModuleFilePath)) { + $aNonMatchingPaths[] = $sCurrentModuleFilePath; + continue; + } + + SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]); return true; } + if (count($aNonMatchingPaths) > 0) { + //add log for support + SetupLog::Info("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]); + } return false; } diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index cf77bdf79a..e0e0c15199 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1602,10 +1602,7 @@ JS $aDirsToScan[] = $sExtraDir; } $oProductionEnv = new RunTimeEnvironment(); - $aRemovedExtensionCodes = $oWizard->GetParameter('removed_extensions', null); - if (! is_array($aRemovedExtensionCodes)) { - $aRemovedExtensionCodes = []; - } + $aRemovedExtensionCodes = $oWizard->GetParameter('removed_extensions', []); $oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan); $oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes); From 9768ffb19d41b08074428e59fea0a920a56f0715 Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 7 Jan 2026 19:57:06 +0100 Subject: [PATCH 16/16] =?UTF-8?q?N=C2=B08981:=20ModuleInstallationReposito?= =?UTF-8?q?ry=20dedicated=20to=20module=20installation=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N°8981: ModuleInstallationRepository dedicated to module installation queries fix renaming --- .../src/Service/DBToolsUtils.php | 43 --------------- datamodels/2.x/itop-hub-connector/launch.php | 2 +- setup/AnalyzeInstallation.php | 4 +- ...e.php => ModuleInstallationRepository.php} | 53 +++++++++++++++++-- setup/runtimeenv.class.inc.php | 2 +- .../itop-hub-connector/HubControllerTest.php | 2 +- .../src/BaseTestCase/ItopDataTestCase.php | 12 ++--- .../setup/AnalyzeInstallationTest.php | 6 +-- 8 files changed, 62 insertions(+), 62 deletions(-) rename setup/{ModuleInstallationService.php => ModuleInstallationRepository.php} (74%) diff --git a/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php b/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php index 5abf29419a..735f714d0a 100644 --- a/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php +++ b/datamodels/2.x/combodo-db-tools/src/Service/DBToolsUtils.php @@ -139,49 +139,6 @@ EOF; return $aValues; } - /** - * Return previous module installation. offset is applied on parent_id. - * @param $iOffset - * @return array - */ - public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array - { - $oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"'); - $oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first - $oSet->SetLimit($iOffset + 1); - - $iParentId = 0; - /** @var \DBObject $oModuleInstallation */ - while ($oModuleInstallation = $oSet->Fetch()) { - if ($iOffset == 0) { - $iParentId = $oModuleInstallation->Get('id'); - break; - } - $iOffset--; - } - - if ($iParentId === 0) { - IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]); - throw new \Exception("no ITOP_APPLICATION ModuleInstallation found"); - } - - $oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId"); - $oSet = new DBObjectSet($oFilter); // Most recent first - $aRawValues = $oSet->ToArrayOfValues(); - $aValues = []; - foreach ($aRawValues as $aRawValue) { - $aValue = []; - foreach ($aRawValue as $sAliasAttCode => $sValue) { - // remove 'mi.' from AttCode - $sAttCode = substr($sAliasAttCode, 3); - $aValue[$sAttCode] = $sValue; - } - - $aValues[] = $aValue; - } - return $aValues; - } - public static function GetDBTablesInfo() { self::AnalyzeTables(); diff --git a/datamodels/2.x/itop-hub-connector/launch.php b/datamodels/2.x/itop-hub-connector/launch.php index ffa20366b1..1f3e520840 100644 --- a/datamodels/2.x/itop-hub-connector/launch.php +++ b/datamodels/2.x/itop-hub-connector/launch.php @@ -186,7 +186,7 @@ function collect_configuration() // iTop modules $oConfig = MetaModel::GetConfig(); - $aInstalledModules = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig); + $aInstalledModules = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig); foreach ($aInstalledModules as $aDBInfo) { $aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version']; diff --git a/setup/AnalyzeInstallation.php b/setup/AnalyzeInstallation.php index ca0c35ea1a..1335c9aaf5 100644 --- a/setup/AnalyzeInstallation.php +++ b/setup/AnalyzeInstallation.php @@ -1,6 +1,6 @@ ReadComputeInstalledModules($oConfig); + $aCurrentlyInstalledModules = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig); // Adjust the list of proposed modules foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) { diff --git a/setup/ModuleInstallationService.php b/setup/ModuleInstallationRepository.php similarity index 74% rename from setup/ModuleInstallationService.php rename to setup/ModuleInstallationRepository.php index 259f884ed2..f98011dd51 100644 --- a/setup/ModuleInstallationService.php +++ b/setup/ModuleInstallationRepository.php @@ -1,23 +1,23 @@ false]); // Most recent first + $oSet->SetLimit($iOffset + 1); + + $iParentId = 0; + /** @var \DBObject $oModuleInstallation */ + while ($oModuleInstallation = $oSet->Fetch()) { + if ($iOffset == 0) { + $iParentId = $oModuleInstallation->Get('id'); + break; + } + $iOffset--; + } + + if ($iParentId === 0) { + IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]); + throw new \Exception("no ITOP_APPLICATION ModuleInstallation found"); + } + + $oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId"); + $oSet = new DBObjectSet($oFilter); // Most recent first + $aRawValues = $oSet->ToArrayOfValues(); + $aValues = []; + foreach ($aRawValues as $aRawValue) { + $aValue = []; + foreach ($aRawValue as $sAliasAttCode => $sValue) { + // remove 'mi.' from AttCode + $sAttCode = substr($sAliasAttCode, 3); + $aValue[$sAttCode] = $sValue; + } + + $aValues[] = $aValue; + } + return $aValues; + } } diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 240d6e43bc..1464fd0581 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -630,7 +630,7 @@ class RunTimeEnvironment public function GetApplicationVersion(Config $oConfig) { try { - $aSelectInstall = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig); + $aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig); } catch (MySQLException $e) { // No database or erroneous information $this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name')); diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php index d14180fc5b..7e1a1580ee 100644 --- a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php @@ -46,7 +46,7 @@ class HubControllerTest extends ItopDataTestCase $this->testLaunchCompile(); HubController::GetInstance()->LaunchDeploy(); $this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}'); - $this->CompareCurrentAndPreviousModuleInstallations(); + $this->AssertPreviousAndCurrentInstallationAreEquivalent(); } private function CheckReport($sExpected) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index ee00f7c46b..9763f7b526 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -35,6 +35,7 @@ use lnkContactToTicket; use lnkFunctionalCIToTicket; use MetaModel; use MissingQueryArgument; +use ModuleInstallationRepository; use MySQLException; use MySQLHasGoneAwayException; use Person; @@ -1558,15 +1559,14 @@ abstract class ItopDataTestCase extends ItopTestCase @unlink($this->sConfigTmpBackupFile); } - public function CompareCurrentAndPreviousModuleInstallations() + public function AssertPreviousAndCurrentInstallationAreEquivalent() { - $this->RequireOnceItopFile('env-production/combodo-db-tools/src/Service/DBToolsUtils.php'); - $aPreviousInstallations = DBToolsUtils::GetPreviousModuleInstallationsByOffset(1); - $aInstallations = DBToolsUtils::GetPreviousModuleInstallationsByOffset(); - $this->assertEquals($this->KeepModuleInstallationComparableFields($aPreviousInstallations), $this->KeepModuleInstallationComparableFields($aInstallations)); + $aPreviousInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset(1); + $aInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset(); + $this->assertEquals($this->GetCanonicalComparableModuleInstallationArray($aPreviousInstallations), $this->GetCanonicalComparableModuleInstallationArray($aInstallations)); } - public function KeepModuleInstallationComparableFields($aInstallations): array + protected function GetCanonicalComparableModuleInstallationArray($aInstallations): array { $aRes = []; $aIgnoredFields = ['id', 'parent_id', 'installed', 'comment']; diff --git a/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php b/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php index 5452d1a342..1f392ff9e2 100644 --- a/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php @@ -4,7 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Setup; use AnalyzeInstallation; use Combodo\iTop\Test\UnitTest\ItopTestCase; -use ModuleInstallationService; +use ModuleInstallationRepository; class AnalyzeInstallationTest extends ItopTestCase { @@ -12,7 +12,7 @@ class AnalyzeInstallationTest extends ItopTestCase { parent::setUp(); $this->RequireOnceItopFile('setup/AnalyzeInstallation.php'); - $this->RequireOnceItopFile('setup/ModuleInstallationService.php'); + $this->RequireOnceItopFile('setup/ModuleInstallationRepository.php'); $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); $this->RequireOnceItopFile('setup/runtimeenv.class.inc.php'); } @@ -151,7 +151,7 @@ class AnalyzeInstallationTest extends ItopTestCase $this->SetNonPublicProperty(AnalyzeInstallation::GetInstance(), "aAvailableModules", $aAvailableModules); //$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true); - $this->SetNonPublicProperty(ModuleInstallationService::GetInstance(), "aSelectInstall", $aInstalledModules); + $this->SetNonPublicProperty(ModuleInstallationRepository::GetInstance(), "aSelectInstall", $aInstalledModules); $oConfig = $this->createMock(\Config::class);