diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index f658b130d..d348597a7 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 aac0aa753..5785d2512 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 d7f362db8..94e318e17 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 000000000..e7c8af96d --- /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 6c334a90d..84058c2d1 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 00cb7158e..048388000 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 017d90bc0..cf77bdf79 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 1ba2cc218..680e3a747 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) {