From aef80d60a88bdf505db56fe87557872c7caa857c Mon Sep 17 00:00:00 2001 From: Timmy38 <101416770+Timmy38@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:13:49 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B010021=20Fix=20handling=20of=20removed=20?= =?UTF-8?q?extension=20that=20are=20also=20part=20of=20product?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup/extensionsmap.class.inc.php | 83 +++++++++++-------- .../wizardsteps/WizStepLandingBeforeAudit.php | 5 +- .../unitary-tests/setup/ExtensionsMapTest.php | 34 ++++++++ .../setup/iTopExtensionsMapFake.php | 24 ++++++ 4 files changed, 111 insertions(+), 35 deletions(-) diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index e53ae57e11..1bf761ac7c 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -526,7 +526,16 @@ class iTopExtensionsMap public function GetAllExtensionsWithPreviouslyInstalled(): array { //Mind the order, local extensions data must overwrite installed extensions data since installed extensions does not have the associated modules. - return array_merge($this->aInstalledExtensions ?? [], $this->aExtensions); + $aResult = []; + foreach ($this->aInstalledExtensions ?? [] as $sKey => $oInstalledExtension) { + /** @var \iTopExtension $oInstalledExtension */ + $aResult[$oInstalledExtension->sSource.'/'.$sKey] = $oInstalledExtension; + } + foreach ($this->aExtensions as $sKey => $oExtension) { + /** @var \iTopExtension $oExtension */ + $aResult[$oExtension->sSource.'/'.$sKey] = $oExtension; + } + return $aResult; } /** @@ -664,7 +673,7 @@ class iTopExtensionsMap return true; } - public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false + protected function FetchExtensionInfoFromDatabase(Config $oConfig): array|false { try { if (CMDBSource::DBName() === null) { @@ -672,41 +681,49 @@ class iTopExtensionsMap } $sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_extension_install"); $aDBInfo = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_extension_install WHERE installed = '".$sLatestInstallationDate."'"); - - $this->aInstalledExtensions = []; - foreach ($aDBInfo as $aExtensionInfo) { - $oExtension = new iTopExtension(); - $oExtension->sCode = $aExtensionInfo['code']; - $oExtension->sLabel = $aExtensionInfo['label']; - $oExtension->sDescription = $aExtensionInfo['description'] ?? ''; - $oExtension->sVersion = $aExtensionInfo['version']; - $oExtension->sSource = $aExtensionInfo['source']; - $oExtension->bMandatory = false; - $oExtension->sMoreInfoUrl = ''; - $oExtension->aModules = []; - $oExtension->aModuleVersion = []; - $oExtension->aModuleInfo = []; - $oExtension->sSourceDir = ''; - $oExtension->bVisible = true; - $oExtension->bInstalled = true; - $oExtension->bCanBeUninstalled = !isset($aExtensionInfo['uninstallable']) || $aExtensionInfo['uninstallable'] === 'yes'; - $oChoice = $this->GetFromExtensionCode($oExtension->sCode); - if ($oChoice) { - $oChoice->bInstalled = true; - $oExtension->bRemovedFromDisk = $oChoice->bRemovedFromDisk; - } else { - $oExtension->bRemovedFromDisk = true; - $this->aExtensionsByCode[$oExtension->sCode] = $oExtension; - } - - $this->aInstalledExtensions[$oExtension->sCode.'/'.$oExtension->sVersion] = $oExtension; - } - - return $this->aInstalledExtensions; } catch (MySQLException $e) { // No database or erroneous information return false; } + return $aDBInfo; + } + + public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false + { + $aDBInfo = $this->FetchExtensionInfoFromDatabase($oConfig); + if (false === $aDBInfo) { + return false; + } + + $this->aInstalledExtensions = []; + foreach ($aDBInfo as $aExtensionInfo) { + $oExtension = new iTopExtension(); + $oExtension->sCode = $aExtensionInfo['code']; + $oExtension->sLabel = $aExtensionInfo['label']; + $oExtension->sDescription = $aExtensionInfo['description'] ?? ''; + $oExtension->sVersion = $aExtensionInfo['version']; + $oExtension->sSource = $aExtensionInfo['source']; + $oExtension->bMandatory = false; + $oExtension->sMoreInfoUrl = ''; + $oExtension->aModules = []; + $oExtension->aModuleVersion = []; + $oExtension->aModuleInfo = []; + $oExtension->sSourceDir = ''; + $oExtension->bVisible = true; + $oExtension->bInstalled = true; + $oExtension->bCanBeUninstalled = !isset($aExtensionInfo['uninstallable']) || $aExtensionInfo['uninstallable'] === 'yes'; + $oChoice = $this->GetFromExtensionCode($oExtension->sCode); + if ($oChoice) { + $oChoice->bInstalled = true; + $oExtension->bRemovedFromDisk = $oChoice->bRemovedFromDisk || $oChoice->sSource !== $oExtension->sSource; + } else { + $oExtension->bRemovedFromDisk = true; + $this->aExtensionsByCode[$oExtension->sCode] = $oExtension; + } + + $this->aInstalledExtensions[$oExtension->sCode.'/'.$oExtension->sVersion] = $oExtension; + } + return $this->aInstalledExtensions; } public function GetChoicesFromDatabase(Config $oConfig): array|false diff --git a/setup/wizardsteps/WizStepLandingBeforeAudit.php b/setup/wizardsteps/WizStepLandingBeforeAudit.php index a00aeebd16..f4d96b3068 100644 --- a/setup/wizardsteps/WizStepLandingBeforeAudit.php +++ b/setup/wizardsteps/WizStepLandingBeforeAudit.php @@ -82,13 +82,14 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice $bForceUninstall = (bool)$this->oWizard->GetParameter('force-uninstall', false); if ($bSkipWizard && !$bForceUninstall) { + $oLatestWizardState = $this->oWizard->GetLatestWizardStateFromStepClass(WizStepModulesChoice::class); // Check if the current choices allow going to the data audit step $aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo(); foreach ($aOptions as $index => $aChoice) { $sChoiceId = self::$SEP.$index; - $oLatestWizardState = $this->oWizard->GetLatestWizardStateFromStepClass(WizStepModulesChoice::class); $aFlags = $this->ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents[$oLatestWizardState->GetState()], false, false); - if (!static::CanMoveForwardFromChoiceFlags($aFlags)) { + if (!static::CanMoveForwardFromChoiceFlags($aFlags) || $aFlags['missing']) { + // If an extension is missing, its uninstallation will be forced. We need to show it to the user // Pop the latest step from the stack, since we are going back to it $this->oWizard->PopStep(); diff --git a/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php b/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php index ff9df153b9..17fb81bb62 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php @@ -359,6 +359,40 @@ XML; $oExtensionMap->CheckExtensionsValidity(); } + public function testExtensionsFoundInDBShouldBeMarkedAsInstalled() + { + $oExtensionsMap = iTopExtensionsMapFake::createFromArray([ + 'itop-installed' => [ + 'installed' => false, + ], + 'itop-not-installed' => [ + 'installed' => false, + ] + ]); + $oExtensionsMap->AddInstalledExtensionInfo('itop-installed'); + $oExtensionsMap->LoadInstalledExtensionsFromDatabase(new \Config()); + + $this->assertTrue($oExtensionsMap->GetFromExtensionCode('itop-installed')->bInstalled); + $this->assertFalse($oExtensionsMap->GetFromExtensionCode('itop-not-installed')->bInstalled); + } + + public function testExtensionSourceMismatchShouldBeFlaggedAsRemoved() + { + $sExtensionCode = 'itop-problem-mgmt'; + $oExtensionsMap = iTopExtensionsMapFake::createFromArray([ + $sExtensionCode => [ + 'installed' => false, + 'source' => iTopExtension::SOURCE_WIZARD, + ] + ]); + $oExtensionsMap->AddInstalledExtensionInfo($sExtensionCode, '6.4.0', iTopExtension::SOURCE_MANUAL); + + $oExtensionsMap->LoadInstalledExtensionsFromDatabase(new \Config()); + + $oInstalledExtension = $oExtensionsMap->aInstalledExtensions[$sExtensionCode.'/6.4.0']; + $this->assertTrue($oInstalledExtension->bRemovedFromDisk); + } + private function CreateFixtureContext(string $sEnvPrefix): array { $sEnvironment = str_replace('.', '-', uniqid($sEnvPrefix, true)); diff --git a/tests/php-unit-tests/unitary-tests/setup/iTopExtensionsMapFake.php b/tests/php-unit-tests/unitary-tests/setup/iTopExtensionsMapFake.php index aed8b3cdaf..08abf71863 100644 --- a/tests/php-unit-tests/unitary-tests/setup/iTopExtensionsMapFake.php +++ b/tests/php-unit-tests/unitary-tests/setup/iTopExtensionsMapFake.php @@ -2,6 +2,10 @@ class iTopExtensionsMapFake extends iTopExtensionsMap { + public $aInstalledExtensionsInfo = false; + public array $aInstalledExtensions; + public $aExtensions; + public function __construct($sFromEnvironment = 'production', $aExtraDirs = []) { $this->aExtensions = []; @@ -33,4 +37,24 @@ class iTopExtensionsMapFake extends iTopExtensionsMap { parent::AddExtension($oNewExtension); } + + protected function FetchExtensionInfoFromDatabase(Config $oConfig): array|false + { + + return $this->aInstalledExtensionsInfo; + } + + public function AddInstalledExtensionInfo(string $sCode, string $sVersion = '1.0.0', string $sSource = iTopExtension::SOURCE_MANUAL, string $sUninstallable = 'yes'): void + { + if ($this->aInstalledExtensionsInfo === false) { + $this->aInstalledExtensionsInfo = []; + } + $this->aInstalledExtensionsInfo[] = [ + 'code' => $sCode, + 'label' => $sCode, + 'version' => $sVersion, + 'source' => $sSource, + 'uninstallable' => $sUninstallable, + ]; + } }