N°10021 Fix handling of removed extension that are also part of product

This commit is contained in:
Timmy38
2026-09-03 10:13:49 +02:00
committed by GitHub
parent 5597040a8a
commit aef80d60a8
4 changed files with 111 additions and 35 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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));

View File

@@ -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,
];
}
}