From e1ea93c8f2224dc12f66270c626ef8024ca2f8bf Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 21 Jan 2026 17:00:24 +0100 Subject: [PATCH] =?UTF-8?q?=20N=C2=B08760=20-=20Audit=20uninstall=20of=20e?= =?UTF-8?q?xtensions=20that=20declare=20final=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N°8760 - be able to list modules based on extension choices refactoring: move some classes in a moduleinstallation folder (coming namespace) N°8760 - module dependency check applied before audit N°8760 - make dependency check work during audit N°8760 - fix ci N°8760 - fix ci N°8760 - add GetCreatedIn to get module name based on DBObject class - everything stored in MetaModel during compilation and autoload N°8760 - be able to describe from which module a datamodel class comes via MetaModel created_in field N°8760 - rename GetCreatedIn <- GetModuleName + compute module name live instead having complex stuff in MetaModel/compilation temp review 1 review: renaming InstallationChoicesToModuleConverter review: renaming InstallationChoicesToModuleConverter review: ModuleDiscovery:GetModulesOrderedByDependencies replacing deprecated GetAvailableModules method ci: fix typo cleanup review: rework InstallationChoicesToModuleConverter N°8760 - review tests --- application/application.inc.php | 2 +- core/autoload.php | 2 +- core/config.class.inc.php | 2 +- core/metamodel.class.php | 33 +- setup/compiler.class.inc.php | 6 +- setup/extensionsmap.class.inc.php | 25 +- .../DryRemovalRuntimeEnvironment.php | 36 +- setup/modelfactory.class.inc.php | 2 +- .../dependencyexpression.class.inc.php | 2 +- setup/modulediscovery.class.inc.php | 202 ++++---- .../AnalyzeInstallation.php | 2 +- .../InstallationChoicesToModuleConverter.php | 215 +++++++++ .../ModuleInstallationException.php | 5 + .../ModuleInstallationRepository.php | 0 .../moduleinstallation.class.inc.php | 0 setup/runtimeenv.class.inc.php | 24 +- setup/setuputils.class.inc.php | 2 +- .../InstallationFileService.php | 2 +- .../php-static-analysis/config/base.dist.neon | 2 +- .../ItopCustomDatamodelTestCase.php | 3 + .../unitary-tests/core/MetaModelTest.php | 28 ++ .../AnalyzeInstallationTest.php | 2 - ...stallationChoicesToModuleConverterTest.php | 430 ++++++++++++++++++ .../analyze_installation_output.json | 0 .../ressources/available_modules.json | 0 .../ressources/installation.xml | 230 ++++++++++ ...installation_choices_when_no_xml_file.json | 1 + .../ressources/priv_modules.json | 0 .../ressources/priv_modules2.json | 0 .../priv_modules_simpleusecase.json | 0 30 files changed, 1139 insertions(+), 119 deletions(-) rename setup/{ => moduleinstallation}/AnalyzeInstallation.php (96%) create mode 100644 setup/moduleinstallation/InstallationChoicesToModuleConverter.php create mode 100644 setup/moduleinstallation/ModuleInstallationException.php rename setup/{ => moduleinstallation}/ModuleInstallationRepository.php (100%) rename setup/{ => moduleinstallation}/moduleinstallation.class.inc.php (100%) rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/AnalyzeInstallationTest.php (97%) create mode 100644 tests/php-unit-tests/unitary-tests/setup/moduleinstallation/InstallationChoicesToModuleConverterTest.php rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/ressources/analyze_installation_output.json (100%) rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/ressources/available_modules.json (100%) create mode 100644 tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation.xml create mode 100644 tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation_choices_when_no_xml_file.json rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/ressources/priv_modules.json (100%) rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/ressources/priv_modules2.json (100%) rename tests/php-unit-tests/unitary-tests/setup/{ => moduleinstallation}/ressources/priv_modules_simpleusecase.json (100%) diff --git a/application/application.inc.php b/application/application.inc.php index c04e8d679..1a8b3f762 100644 --- a/application/application.inc.php +++ b/application/application.inc.php @@ -16,5 +16,5 @@ require_once(APPROOT.'/application/audit.category.class.inc.php'); require_once(APPROOT.'/application/audit.domain.class.inc.php'); require_once(APPROOT.'/application/audit.rule.class.inc.php'); require_once(APPROOT.'/application/query.class.inc.php'); -require_once(APPROOT.'/setup/moduleinstallation.class.inc.php'); +require_once(APPROOT.'/setup/moduleinstallation/moduleinstallation.class.inc.php'); require_once(APPROOT.'/application/utils.inc.php'); diff --git a/core/autoload.php b/core/autoload.php index 83f4e972c..b244272b8 100644 --- a/core/autoload.php +++ b/core/autoload.php @@ -24,7 +24,7 @@ MetaModel::IncludeModule('application/user.dashboard.class.inc.php'); MetaModel::IncludeModule('application/audit.rule.class.inc.php'); MetaModel::IncludeModule('application/audit.domain.class.inc.php'); MetaModel::IncludeModule('application/query.class.inc.php'); -MetaModel::IncludeModule('setup/moduleinstallation.class.inc.php'); +MetaModel::IncludeModule('setup/moduleinstallation/moduleinstallation.class.inc.php'); MetaModel::IncludeModule('core/event.class.inc.php'); MetaModel::IncludeModule('core/action.class.inc.php'); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 970b7e3b7..be7a95663 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -2766,7 +2766,7 @@ class Config $oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values $aAddOns = $oEmptyConfig->GetAddOns(); - $aModules = ModuleDiscovery::GetAvailableModules([APPROOT.$sModulesDir]); + $aModules = ModuleDiscovery::GetModulesOrderedByDependencies([APPROOT.$sModulesDir]); foreach ($aModules as $sModuleId => $aModuleInfo) { list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); if (is_null($aSelectedModules) || in_array($sModuleName, $aSelectedModules)) { diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 4953299f1..1f5a76a50 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -22,6 +22,8 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents; use Combodo\iTop\Core\MetaModel\FriendlyNameType; use Combodo\iTop\Service\Events\EventData; use Combodo\iTop\Service\Events\EventService; +use Combodo\iTop\Setup\ModuleDependency\Module; +use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader; require_once APPROOT.'core/modulehandler.class.inc.php'; require_once APPROOT.'core/querymodifier.class.inc.php'; @@ -468,11 +470,35 @@ abstract class MetaModel * @return string * @throws \CoreException */ - final public static function GetCreatedIn($sClass) + final public static function GetModuleName($sClass) { - self::_check_subclass($sClass); + try { + $oReflectionClass = new ReflectionClass($sClass); + $sDir = realpath(dirname($oReflectionClass->getFileName())); + $sApproot = realpath(APPROOT); + while (($sDir !== $sApproot) && (str_contains($sDir, $sApproot))) { + $aFiles = glob("$sDir/module.*.php"); + if (count($aFiles) > 1) { + return 'core'; + } - return self::$m_aClassParams[$sClass]["created_in"] ?? ""; + if (count($aFiles) == 0) { + $sDir = realpath(dirname($sDir)); + continue; + } + + $sModuleFilePath = $aFiles[0]; + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath); + $sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID]; + list($sModuleName, ) = ModuleDiscovery::GetModuleName($sModuleId); + + return $sModuleName; + } + } catch (\Exception $e) { + throw new CoreException("Cannot find class module", ['class' => $sClass], '', $e); + } + + return 'core'; } /** @@ -3158,7 +3184,6 @@ abstract class MetaModel $aMandatParams = [ "category" => "group classes by modules defining their visibility in the UI", "key_type" => "autoincrement | string", - //"created_in" => "module_name where class is defined", "name_attcode" => "define which attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'", "state_attcode" => "define which attribute is representing the state (object lifecycle)", "reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes", diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index edbc37214..7c4741a80 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -477,7 +477,7 @@ class MFCompiler $sClass = $oClass->getAttribute("id"); $aAllClasses[] = $sClass; try { - $sCompiledCode .= $this->CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir); + $sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir); } catch (DOMFormatException $e) { $sMessage = "Failed to process class '$sClass', "; if (!empty($sModuleRootDir)) { @@ -1189,7 +1189,6 @@ EOF /** * @param \MFElement $oClass - * @param string $sModuleName * @param string $sTempTargetDir * @param string $sFinalTargetDir * @param string $sModuleRelativeDir @@ -1197,7 +1196,7 @@ EOF * @return string * @throws \DOMFormatException */ - protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir) + protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir) { $sClass = $oClass->getAttribute('id'); $oProperties = $oClass->GetUniqueElement('properties'); @@ -1210,7 +1209,6 @@ EOF $aClassParams = []; $aClassParams['category'] = $this->GetPropString($oProperties, 'category', ''); $aClassParams['key_type'] = "'autoincrement'"; - $aClassParams['created_in'] = "'$sModuleName'"; if ((bool)$this->GetPropNumber($oProperties, 'is_link', 0)) { $aClassParams['is_link'] = 'true'; } diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index a187df516..9b145ffd6 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -194,7 +194,7 @@ class iTopExtensionsMap } } - \ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension); + ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension); } /** @@ -329,7 +329,7 @@ class iTopExtensionsMap $aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs); try { - ModuleDiscovery::GetAvailableModules($aSearchDirs, true); + ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true); } catch (MissingDependencyException $e) { // Some modules have missing dependencies // Let's check what is the impact at the "extensions" level @@ -566,6 +566,27 @@ class iTopExtensionsMap } } + public static function GetChoicesFromDatabase(Config $oConfig): array|false + { + try { + if (CMDBSource::DBName() === null) { + CMDBSource::InitFromConfig($oConfig); + } + $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."'"); + + $aChoices = []; + foreach ($aDBInfo as $aExtensionInfo) { + $aChoices[] = $aExtensionInfo['label']; + } + + return $aChoices; + } catch (MySQLException $e) { + // No database or erroneous information + return false; + } + } + /** * Tells if the given module name is "chosen" since it is part of a "chosen" extension (in the specified source dir) * @param string $sModuleNameToFind diff --git a/setup/feature_removal/DryRemovalRuntimeEnvironment.php b/setup/feature_removal/DryRemovalRuntimeEnvironment.php index 94e318e17..22a7286e1 100644 --- a/setup/feature_removal/DryRemovalRuntimeEnvironment.php +++ b/setup/feature_removal/DryRemovalRuntimeEnvironment.php @@ -2,10 +2,15 @@ namespace Combodo\iTop\Setup\FeatureRemoval; +use Combodo\iTop\Setup\ModuleDependency\Module; +use Config; +use InstallationChoicesToModuleConverter; use iTopExtensionsMap; use MetaModel; +use ModuleDiscovery; use RunTimeEnvironment; use SetupUtils; +use utils; class DryRemovalRuntimeEnvironment extends RunTimeEnvironment { @@ -37,14 +42,22 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment $sEnv = $this->sFinalEnv; $this->aExtensionsByCode = $aExtensionCodesToRemove; - //SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules"); + $this->Cleanup(); SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules"); $this->DeclareExtensionAsRemoved($aExtensionCodesToRemove); + $oDryRemovalConfig = clone(MetaModel::GetConfig()); $oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv); $this->WriteConfigFileSafe($oDryRemovalConfig); + + $sSourceDir = $oDryRemovalConfig->Get('source_dir'); + $aSearchDirs = $this->GetExtraDirsToCompile($sSourceDir); + + $aModulesToLoad = $this->GetModulesToLoad($sSourceEnv, $aSearchDirs); + + ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true, $aModulesToLoad); } private function DeclareExtensionAsRemoved(array $aExtensionCodes): void @@ -53,6 +66,27 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment $oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes); } + private function GetModulesToLoad(string $sSourceEnv, $aSearchDirs): array + { + $oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv)); + $aChoices = iTopExtensionsMap::GetChoicesFromDatabase($oSourceConfig); + $sSourceDir = $oSourceConfig->Get('source_dir'); + + $sInstallFilePath = APPROOT.$sSourceDir.'/installation.xml'; + if (! is_file($sInstallFilePath)) { + $sInstallFilePath = null; + } + + $aModuleIdsToLoad = InstallationChoicesToModuleConverter::GetInstance()->GetModules($aChoices, $aSearchDirs, $sInstallFilePath); + $aModulesToLoad = []; + foreach ($aModuleIdsToLoad as $sModuleId) { + $oModule = new Module($sModuleId); + $sModuleName = $oModule->GetModuleName(); + $aModulesToLoad[] = $sModuleName; + } + return $aModulesToLoad; + } + public function Cleanup() { $sEnv = $this->sFinalEnv; diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index 85bd28e88..98a6c38fc 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -1801,7 +1801,7 @@ EOF */ public function FindModules() { - $aAvailableModules = ModuleDiscovery::GetAvailableModules($this->aRootDirs); + $aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($this->aRootDirs); $aResult = []; foreach ($aAvailableModules as $sId => $aModule) { $oModule = new MFModule($sId, $aModule['root_dir'], $aModule['label'], isset($aModule['auto_select'])); diff --git a/setup/moduledependency/dependencyexpression.class.inc.php b/setup/moduledependency/dependencyexpression.class.inc.php index 89988fb92..f1792479c 100644 --- a/setup/moduledependency/dependencyexpression.class.inc.php +++ b/setup/moduledependency/dependencyexpression.class.inc.php @@ -61,7 +61,7 @@ class DependencyExpression } } - private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator + public static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator { if (!isset(self::$oPhpExpressionEvaluator)) { self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST); diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index d9647677e..0a6ea4f19 100755 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -96,7 +96,7 @@ class ModuleDiscovery protected static $m_aModuleVersionByName = []; /** @var array<\iTopExtension> $m_aRemovedExtensions */ - protected static $m_aRemovedExtensions = []; + protected static array $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']; @@ -196,21 +196,6 @@ class ModuleDiscovery } } - /** - * Get the list of "discovered" modules, ordered based on their (inter) dependencies - * - * @param bool $bAbortOnMissingDependency ... - * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted - * - * @return array - * @throws \MissingDependencyException - */ - protected static function GetModules($bAbortOnMissingDependency = false, $aModulesToLoad = null) - { - // Order the modules to take into account their inter-dependencies - return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad); - } - /** * Arrange an list of modules, based on their (inter) dependencies * @param array $aModules The list of modules to process: 'id' => $aModuleInfo @@ -238,6 +223,7 @@ class ModuleDiscovery } } } + return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency); } @@ -245,7 +231,7 @@ class ModuleDiscovery * @param array<\iTopExtension> $aRemovedExtension * @return void */ - public static function DeclareRemovedExtensions(array $aRemovedExtension) + public static function DeclareRemovedExtensions(array $aRemovedExtension): void { if (self::$m_aRemovedExtensions != $aRemovedExtension) { self::ResetCache(); @@ -253,79 +239,7 @@ class ModuleDiscovery self::$m_aRemovedExtensions = $aRemovedExtension; } - /** - * @param array<\iTopExtension> $aExtensions - * @param string $sModuleName - * @param string $sModuleVersion - * @param array $aModuleInfo - * - * @return bool - */ - private static function IsModuleInExtensionList(array $aExtensions, string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool - { - if (count($aExtensions) === 0) { - return false; - } - $aNonMatchingPaths = []; - $sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; - - /** @var \iTopExtension $oExtension */ - foreach ($aExtensions as $oExtension) { - $sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null; - if (is_null($sCurrentVersion)) { - continue; - } - - if ($sModuleVersion !== $sCurrentVersion) { - continue; - } - - $aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null; - if (is_null($aCurrentModuleInfo)) { - SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen"); - continue; - } - - // 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::Debug("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; - } - - private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator - { - if (!isset(self::$oPhpExpressionEvaluator)) { - self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST); - } - - return self::$oPhpExpressionEvaluator; - } - - /** - * Search (on the disk) for all defined iTop modules, load them and returns the list (as an array) - * of the possible iTop modules to install - * - * @param $aSearchDirs array of directories to search (absolute paths) - * @param bool $bAbortOnMissingDependency ... - * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted - * - * @return array A big array moduleID => ModuleData - * @throws \Exception - */ - public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + private static function Init($aSearchDirs): void { if (self::$m_aSearchDirs != $aSearchDirs) { self::ResetCache(); @@ -344,13 +258,60 @@ class ModuleDiscovery clearstatcache(); self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir)); } - return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad); - } else { - // Reuse the previous results - return self::GetModules($bAbortOnMissingDependency, $aModulesToLoad); } } + /** + * Return all modules found on disk ordered by dependencies. Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions) + * @param $aSearchDirs array of directories to search (absolute paths) + * @param bool $bAbortOnMissingDependency ... + * @param array $aModulesToLoad List of modules to search for, defaults to all if omitted + * + * @return array A big array moduleID => ModuleData + * @throws \Exception + */ + public static function GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + { + self::Init($aSearchDirs); + + return self::OrderModulesByDependencies(self::$m_aModules, $bAbortOnMissingDependency, $aModulesToLoad); + } + + /** + * @deprecated use \ModuleDiscovery::GetModulesOrderedByDependencies instead + */ + public static function GetAvailableModules($aSearchDirs, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + { + return ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, $bAbortOnMissingDependency, $aModulesToLoad); + } + + /** + * Return all modules found on disk (without any dependency consideration). Skipping modules coming from extensions declared as removed (@see ModuleDiscovery::DeclareRemovedExtensions) + * + * @param $aSearchDirs array of directories to search (absolute paths) + * + * @return array A big array moduleID => ModuleData + * @throws \Exception + */ + public static function GetAllModules($aSearchDirs) + { + self::Init($aSearchDirs); + + $aNonRemovedModules = []; + foreach (self::$m_aModules as $sModuleId => $aModuleInfo) { + $oModule = new Module($sModuleId); + $sModuleName = $oModule->GetModuleName(); + + if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) { + continue; + } + + $aNonRemovedModules[$sModuleId] = $aModuleInfo; + } + + return $aNonRemovedModules; + } + public static function ResetCache() { self::$m_aSearchDirs = null; @@ -419,6 +380,59 @@ class ModuleDiscovery throw new Exception("Data directory (".$sDirectory.") not found or not readable."); } } + + /** + * @param array<\iTopExtension> $aExtensions + * @param string $sModuleName + * @param string $sModuleVersion + * @param array $aModuleInfo + * + * @return bool + */ + private static function IsModuleInExtensionList(array $aExtensions, string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool + { + if (count($aExtensions) === 0) { + return false; + } + $aNonMatchingPaths = []; + $sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH]; + + /** @var \iTopExtension $oExtension */ + foreach ($aExtensions as $oExtension) { + $sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null; + if (is_null($sCurrentVersion)) { + continue; + } + + if ($sModuleVersion !== $sCurrentVersion) { + continue; + } + + $aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null; + if (is_null($aCurrentModuleInfo)) { + SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen"); + continue; + } + + // 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::Debug("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; + } + } // End of class /** Alias for backward compatibility with old module files in which diff --git a/setup/AnalyzeInstallation.php b/setup/moduleinstallation/AnalyzeInstallation.php similarity index 96% rename from setup/AnalyzeInstallation.php rename to setup/moduleinstallation/AnalyzeInstallation.php index a1947b7df..6a30e6edf 100644 --- a/setup/AnalyzeInstallation.php +++ b/setup/moduleinstallation/AnalyzeInstallation.php @@ -73,7 +73,7 @@ class AnalyzeInstallation //test only $aAvailableModules = $this->aAvailableModules; } else { - $aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad); + $aAvailableModules = ModuleDiscovery::GetModulesOrderedByDependencies($aDirs, $bAbortOnMissingDependency, $aModulesToLoad); } foreach ($aAvailableModules as $sModuleId => $aModuleInfo) { diff --git a/setup/moduleinstallation/InstallationChoicesToModuleConverter.php b/setup/moduleinstallation/InstallationChoicesToModuleConverter.php new file mode 100644 index 000000000..afb88d939 --- /dev/null +++ b/setup/moduleinstallation/InstallationChoicesToModuleConverter.php @@ -0,0 +1,215 @@ +Get('steps', []); + if (!is_array($aSteps)) { + return []; + } + $aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices, $aSteps); + } else { + $aInstalledModuleNames = $this->FindInstalledPackageModules($aPackageModules, $aInstallationChoices); + } + + $aInstalledModules = []; + foreach (array_keys($aPackageModules) as $sModuleId) { + list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId); + if (in_array($sModuleName, $aInstalledModuleNames)) { + $aInstalledModules[] = $sModuleId; + } + } + + return $aInstalledModules; + } + + private function FindInstalledPackageModules(array $aPackageModules, array $aInstallationChoices, array $aInstallationDescription = null): array + { + $aInstalledModules = []; + + $this->ProcessDefaultModules($aPackageModules, $aInstalledModules); + + if (is_null($aInstallationDescription)) { + //in legacy usecase: choices are flat modules list already + foreach ($aInstallationChoices as $sModuleName) { + $aInstalledModules[$sModuleName] = true; + } + } else { + $this->GetModuleNamesFromInstallationChoices($aInstallationChoices, $aInstallationDescription, $aInstalledModules); + } + + $this->ProcessAutoSelectModules($aPackageModules, $aInstalledModules); + + return array_keys($aInstalledModules); + } + + private function IsDefaultModule(string $sModuleId, array $aModule): bool + { + if (($sModuleId === ROOT_MODULE)) { + return false; + } + + if (isset($aModule['auto_select'])) { + return false; + } + + if ($aModule['category'] === 'authentication') { + return true; + } + + return !$aModule['visible']; + } + + private function ProcessDefaultModules(array &$aPackageModules, array &$aInstalledModules): void + { + foreach ($aPackageModules as $sModuleId => $aModule) { + if ($this->IsDefaultModule($sModuleId, $aModule)) { + list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId); + $aInstalledModules[$sModuleName] = true; + unset($aPackageModules[$sModuleId]); + } + } + } + + private function IsAutoSelectedModule(array $aInstalledModules, string $sModuleId, array $aModule): bool + { + if (($sModuleId === ROOT_MODULE)) { + return false; + } + + if (!isset($aModule['auto_select'])) { + return false; + } + + try { + SetupInfo::SetSelectedModules($aInstalledModules); + return DependencyExpression::GetPhpExpressionEvaluator()->ParseAndEvaluateBooleanExpression($aModule['auto_select']); + } catch (Exception $e) { + IssueLog::Error('Error evaluating module auto-select', null, [ + 'module' => $sModuleId, + 'error' => $e->getMessage(), + 'evaluated code' => $aModule['auto_select'], + 'stacktrace' => $e->getTraceAsString(), + ]); + } + + return false; + } + + private function ProcessAutoSelectModules(array $aPackageModules, array &$aInstalledModules): void + { + foreach ($aPackageModules as $sModuleId => $aModule) { + if ($this->IsAutoSelectedModule($aInstalledModules, $sModuleId, $aModule)) { + list($sModuleName) = ModuleDiscovery::GetModuleName($sModuleId); + $aInstalledModules[$sModuleName] = true; + } + } + } + + private function GetModuleNamesFromInstallationChoices(array $aInstallationChoices, array $aInstallationDescription, array &$aModuleNames): void + { + foreach ($aInstallationDescription as $aStepInfo) { + $aOptions = $aStepInfo['options'] ?? null; + if (is_array($aOptions)) { + foreach ($aOptions as $aChoiceInfo) { + $this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames); + } + } + $aOptions = $aStepInfo['alternatives'] ?? null; + if (is_array($aOptions)) { + foreach ($aOptions as $aChoiceInfo) { + $this->ProcessSelectedChoice($aInstallationChoices, $aChoiceInfo, $aModuleNames); + } + } + } + } + + private function ProcessSelectedChoice(array $aInstallationChoices, array $aChoiceInfo, array &$aInstalledModules) + { + if (!is_array($aChoiceInfo)) { + return; + } + + $sMandatory = $aChoiceInfo['mandatory'] ?? 'false'; + + $aCurrentModules = $aChoiceInfo['modules'] ?? []; + $sExtensionCode = $aChoiceInfo['extension_code']; + + $bSelected = ($sMandatory === 'true') || in_array($sExtensionCode, $aInstallationChoices); + + if (!$bSelected) { + return; + } + + foreach ($aCurrentModules as $sModuleId) { + $aInstalledModules[$sModuleId] = true; + } + + $aAlternatives = $aChoiceInfo['alternatives'] ?? null; + if (is_array($aAlternatives)) { + foreach ($aAlternatives as $aSubChoiceInfo) { + $this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules); + } + } + + $aSubOptionsChoiceInfo = $aChoiceInfo['sub_options'] ?? null; + if (is_array($aSubOptionsChoiceInfo)) { + $aSubOptions = $aSubOptionsChoiceInfo['options'] ?? null; + if (is_array($aSubOptions)) { + foreach ($aSubOptions as $aSubChoiceInfo) { + $this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules); + } + } + $aSubAlternatives = $aSubOptionsChoiceInfo['alternatives'] ?? null; + if (is_array($aSubAlternatives)) { + foreach ($aSubAlternatives as $aSubChoiceInfo) { + $this->ProcessSelectedChoice($aInstallationChoices, $aSubChoiceInfo, $aInstalledModules); + } + } + } + } +} diff --git a/setup/moduleinstallation/ModuleInstallationException.php b/setup/moduleinstallation/ModuleInstallationException.php new file mode 100644 index 000000000..40fdccc83 --- /dev/null +++ b/setup/moduleinstallation/ModuleInstallationException.php @@ -0,0 +1,5 @@ +GetLoadedFile(); if (strlen($sConfigFile) > 0) { @@ -225,12 +226,29 @@ class RunTimeEnvironment return ($oExtension->sSource == iTopExtension::SOURCE_REMOTE); } + public function GetExtraDirsToCompile(string $sSourceDir) : array { + $sSourceDirFull = APPROOT.$sSourceDir; + if (!is_dir($sSourceDirFull)) { + throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)"); + } + $aDirsToCompile = [$sSourceDirFull]; + + if (is_dir(APPROOT.'extensions')) { + $aDirsToCompile[] = APPROOT.'extensions'; + } + $sExtraDir = utils::GetDataPath().$this->sTargetEnv.'-modules/'; + if (is_dir($sExtraDir)) { + $aDirsToCompile[] = $sExtraDir; + } + + return $aDirsToCompile; + } + /** * Get the installed modules (only the installed ones) */ protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir) { - \SetupLog::Info(__METHOD__); $sSourceDirFull = APPROOT.$sSourceDir; if (!is_dir($sSourceDirFull)) { throw new Exception("The source directory '$sSourceDirFull' does not exist (or could not be read)"); diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 6bce7ba82..6378f4179 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -509,7 +509,7 @@ class SetupUtils } require_once(APPROOT.'setup/modulediscovery.class.inc.php'); try { - ModuleDiscovery::GetAvailableModules($aDirsToScan, true, $aSelectedModules); + ModuleDiscovery::GetModulesOrderedByDependencies($aDirsToScan, true, $aSelectedModules); } catch (Exception $e) { $aResult[] = new CheckResult(CheckResult::ERROR, $e->getMessage()); } diff --git a/setup/unattended-install/InstallationFileService.php b/setup/unattended-install/InstallationFileService.php index e1d2494b0..a4fa65c98 100644 --- a/setup/unattended-install/InstallationFileService.php +++ b/setup/unattended-install/InstallationFileService.php @@ -259,7 +259,7 @@ class InstallationFileService { $sProductionModuleDir = APPROOT.'data/'.$this->sTargetEnvironment.'-modules/'; - $aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs(), false, null); + $aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation(MetaModel::GetConfig(), $this->GetExtraDirs()); $this->aAutoSelectModules = []; foreach ($aAvailableModules as $sModuleId => $aModule) { diff --git a/tests/php-static-analysis/config/base.dist.neon b/tests/php-static-analysis/config/base.dist.neon index eaf30b872..3c1d8a66f 100644 --- a/tests/php-static-analysis/config/base.dist.neon +++ b/tests/php-static-analysis/config/base.dist.neon @@ -1,7 +1,7 @@ includes: - php-includes/set-php-version-from-process.php # Workaround to set PHP version to the on running the CLI # for an explanation of the baseline concept, see: https://phpstan.org/user-guide/baseline - #baseline HERE DO NOT REMOVE FOR CI + #baseline HERE DO NOT REMOVE FOR CI parameters: level: 0 diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php index d35190ffa..7034cb5b5 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopCustomDatamodelTestCase.php @@ -188,6 +188,9 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase CMDBSource::DropTable("priv_module_install"); CMDBSource::Query("CREATE TABLE $sNewDB.priv_module_install SELECT * FROM $sPreviousDB.priv_module_install"); + CMDBSource::DropTable("priv_extension_install"); + CMDBSource::Query("CREATE TABLE $sNewDB.priv_extension_install SELECT * FROM $sPreviousDB.priv_extension_install"); + $this->debug("Custom environment '$sTestEnv' is ready!"); } else { $this->debug("Custom environment '$sTestEnv' READY BUILT:"); diff --git a/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php b/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php index 74c947731..ed14c9c2b 100644 --- a/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php +++ b/tests/php-unit-tests/unitary-tests/core/MetaModelTest.php @@ -498,6 +498,34 @@ class MetaModelTest extends ItopDataTestCase 'Purge 10 items with a max_chunk_size of 1000 (default value) should be perfomed in 1 step' => [1000, 3], ]; } + + public function testGetCreatedIn_UnknownClass() + { + $this->expectExceptionMessage("Cannot find class module"); + $this->expectException(CoreException::class); + + MetaModel::GetModuleName('GABUZOMEU'); + } + + public function testGetCreatedIn_ClassComingFromCorePhpFile() + { + $this->assertEquals('core', MetaModel::GetModuleName('BackgroundTask')); + } + + public function testGetCreatedIn_ClassComingFromCorePhpFile2() + { + $this->assertEquals('core', MetaModel::GetModuleName('lnkActionNotificationToContact')); + } + + public function testGetCreatedIn_ClassComingFromModulePhpFile() + { + $this->assertEquals('itop-attachments', MetaModel::GetModuleName('CMDBChangeOpAttachmentAdded')); + } + + public function testGetCreatedIn_ClassComingFromXmlDataModelFile() + { + $this->assertEquals('authent-ldap', MetaModel::GetModuleName('UserLDAP')); + } } abstract class Wizzard diff --git a/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/AnalyzeInstallationTest.php similarity index 97% rename from tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/AnalyzeInstallationTest.php index 1f392ff9e..3d855e69d 100644 --- a/tests/php-unit-tests/unitary-tests/setup/AnalyzeInstallationTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/AnalyzeInstallationTest.php @@ -11,8 +11,6 @@ class AnalyzeInstallationTest extends ItopTestCase protected function setUp(): void { parent::setUp(); - $this->RequireOnceItopFile('setup/AnalyzeInstallation.php'); - $this->RequireOnceItopFile('setup/ModuleInstallationRepository.php'); $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); $this->RequireOnceItopFile('setup/runtimeenv.class.inc.php'); } diff --git a/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/InstallationChoicesToModuleConverterTest.php b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/InstallationChoicesToModuleConverterTest.php new file mode 100644 index 000000000..756d1a176 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/InstallationChoicesToModuleConverterTest.php @@ -0,0 +1,430 @@ +RequireOnceItopFile('/setup/moduleinstallation/InstallationChoicesToModuleConverter.php'); + } + + protected function tearDown(): void + { + parent::tearDown(); + ModuleDiscovery::ResetCache(); + } + + //integration test + public function testGetModulesWithXmlInstallationFile_UsualCustomerPackagesWithNonITIL() + { + $aSearchDirs = $this->GivenModuleDiscoveryInit(); + + $aInstalledModules = InstallationChoicesToModuleConverter::GetInstance()->GetModules( + $this->GivenNonItilChoices(), + $aSearchDirs, + __DIR__.'/ressources/installation.xml' + ); + + $aExpected = [ + 'authent-cas/3.3.0', + 'authent-external/3.3.0', + 'authent-ldap/3.3.0', + 'authent-local/3.3.0', + 'combodo-backoffice-darkmoon-theme/3.3.0', + 'combodo-backoffice-fullmoon-high-contrast-theme/3.3.0', + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme/3.3.0', + 'combodo-backoffice-fullmoon-tritanopia-theme/3.3.0', + 'itop-attachments/3.3.0', + 'itop-backup/3.3.0', + 'itop-config/3.3.0', + 'itop-files-information/3.3.0', + 'itop-portal-base/3.3.0', + 'itop-portal/3.3.0', + 'itop-profiles-itil/3.3.0', + 'itop-sla-computation/3.3.0', + 'itop-structure/3.3.0', + 'itop-themes-compat/3.3.0', + 'itop-tickets/3.3.0', + 'itop-welcome-itil/3.3.0', + 'combodo-db-tools/3.3.0', + 'itop-config-mgmt/3.3.0', + 'itop-core-update/3.3.0', + 'itop-datacenter-mgmt/3.3.0', + 'itop-endusers-devices/3.3.0', + 'itop-faq-light/3.3.0', + 'itop-hub-connector/3.3.0', + 'itop-knownerror-mgmt/3.3.0', + 'itop-oauth-client/3.3.0', + 'itop-request-mgmt/3.3.0', + 'itop-service-mgmt/3.3.0', + 'itop-storage-mgmt/3.3.0', + 'itop-virtualization-mgmt/3.3.0', + 'itop-bridge-cmdb-services/3.3.0', + 'itop-bridge-cmdb-ticket/3.3.0', + 'itop-bridge-datacenter-mgmt-services/3.3.0', + 'itop-bridge-endusers-devices-services/3.3.0', + 'itop-bridge-storage-mgmt-services/3.3.0', + 'itop-bridge-virtualization-mgmt-services/3.3.0', + 'itop-bridge-virtualization-storage/3.3.0', + 'itop-change-mgmt/3.3.0', + ]; + $this->assertEquals($aExpected, $aInstalledModules); + } + + //integration test + public function testGetModulesWithXmlInstallationFile_UsualCustomerPackagesWithITIL() + { + $aSearchDirs = $this->GivenModuleDiscoveryInit(); + + $aInstalledModules = InstallationChoicesToModuleConverter::GetInstance()->GetModules( + $this->GivenItilChoices(), + $aSearchDirs, + __DIR__.'/ressources/installation.xml' + ); + + $aExpected = [ + 'authent-cas/3.3.0', + 'authent-external/3.3.0', + 'authent-ldap/3.3.0', + 'authent-local/3.3.0', + 'combodo-backoffice-darkmoon-theme/3.3.0', + 'combodo-backoffice-fullmoon-high-contrast-theme/3.3.0', + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme/3.3.0', + 'combodo-backoffice-fullmoon-tritanopia-theme/3.3.0', + 'itop-attachments/3.3.0', + 'itop-backup/3.3.0', + 'itop-config/3.3.0', + 'itop-files-information/3.3.0', + 'itop-portal-base/3.3.0', + 'itop-portal/3.3.0', + 'itop-profiles-itil/3.3.0', + 'itop-sla-computation/3.3.0', + 'itop-structure/3.3.0', + 'itop-themes-compat/3.3.0', + 'itop-tickets/3.3.0', + 'itop-welcome-itil/3.3.0', + 'combodo-db-tools/3.3.0', + 'itop-config-mgmt/3.3.0', + 'itop-core-update/3.3.0', + 'itop-datacenter-mgmt/3.3.0', + 'itop-endusers-devices/3.3.0', + 'itop-hub-connector/3.3.0', + 'itop-incident-mgmt-itil/3.3.0', + 'itop-oauth-client/3.3.0', + 'itop-request-mgmt-itil/3.3.0', + 'itop-service-mgmt/3.3.0', + 'itop-storage-mgmt/3.3.0', + 'itop-virtualization-mgmt/3.3.0', + 'itop-bridge-cmdb-services/3.3.0', + 'itop-bridge-cmdb-ticket/3.3.0', + 'itop-bridge-datacenter-mgmt-services/3.3.0', + 'itop-bridge-endusers-devices-services/3.3.0', + 'itop-bridge-storage-mgmt-services/3.3.0', + 'itop-bridge-virtualization-mgmt-services/3.3.0', + 'itop-bridge-virtualization-storage/3.3.0', + 'itop-change-mgmt-itil/3.3.0', + 'itop-full-itil/3.3.0', + ]; + $this->assertEquals($aExpected, $aInstalledModules); + } + + //integration test + public function testGetModulesWithXmlInstallationFile_LegacyPackages() + { + $aSearchDirs = $this->GivenModuleDiscoveryInit(); + + //no choices means all default ones... + $aNoInstallationChoices = []; + + $aInstalledModules = InstallationChoicesToModuleConverter::GetInstance()->GetModules( + $aNoInstallationChoices, + $aSearchDirs + ); + + $aExpected = [ + 'authent-cas/3.3.0', + 'authent-external/3.3.0', + 'authent-ldap/3.3.0', + 'authent-local/3.3.0', + 'combodo-backoffice-darkmoon-theme/3.3.0', + 'combodo-backoffice-fullmoon-high-contrast-theme/3.3.0', + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme/3.3.0', + 'combodo-backoffice-fullmoon-tritanopia-theme/3.3.0', + 'itop-backup/3.3.0', + 'itop-config/3.3.0', + 'itop-files-information/3.3.0', + 'itop-portal-base/3.3.0', + 'itop-profiles-itil/3.3.0', + 'itop-sla-computation/3.3.0', + 'itop-structure/3.3.0', + 'itop-welcome-itil/3.3.0', + ]; + $this->assertEquals($aExpected, $aInstalledModules); + } + + public function testIsDefaultModule_RootModuleShouldNeverBeDefault() + { + $sModuleId = ROOT_MODULE; + $aModuleInfo = ['category' => 'authentication', 'visible' => false]; + $this->assertFalse($this->CallIsDefault($sModuleId, $aModuleInfo)); + } + + public function testIsDefaultModule_AutoselectShouldNeverBeDefault() + { + $sModuleId = 'autoselect_module'; + $aModuleInfo = ['category' => 'authentication', 'visible' => false, 'auto_select' => true]; + $this->assertFalse($this->CallIsDefault($sModuleId, $aModuleInfo)); + } + + public function testIsDefaultModule_AuthenticationModuleShouldBeDefault() + { + $sModuleId = 'authentication_module'; + $aModuleInfo = ['category' => 'authentication', 'visible' => true]; + $this->assertTrue($this->CallIsDefault($sModuleId, $aModuleInfo)); + } + + public function testIsDefaultModule_HiddenModuleShouldBeDefault() + { + $sModuleId = 'hidden_module'; + $aModuleInfo = ['category' => 'business', 'visible' => false]; + $this->assertTrue($this->CallIsDefault($sModuleId, $aModuleInfo)); + } + + public function testIsDefaultModule_NonModuleDefaultCase() + { + $sModuleId = 'any_module'; + $aModuleInfo = ['category' => 'business', 'visible' => true]; + $this->assertFalse($this->CallIsDefault($sModuleId, $aModuleInfo)); + } + + private function CallIsDefault($sModuleId, $aModuleInfo): bool + { + return $this->InvokeNonPublicMethod(InstallationChoicesToModuleConverter::class, 'IsDefaultModule', InstallationChoicesToModuleConverter::GetInstance(), [$sModuleId, $aModuleInfo]); + } + + public function testIsAutoSelectedModule_RootModuleShouldNeverBeAutoSelect() + { + $sModuleId = ROOT_MODULE; + $aModuleInfo = ['auto_select' => true]; + $this->assertFalse($this->CallIsAutoSelectedModule([], $sModuleId, $aModuleInfo)); + } + + public function testIsAutoSelectedModule_NoAutoselectByDefault() + { + $sModuleId = 'autoselect_module'; + $aModuleInfo = []; + $this->assertFalse($this->CallIsAutoSelectedModule([], $sModuleId, $aModuleInfo)); + } + + /** + * @return void + * cf DependencyExpression dedicated tests + */ + public function testIsAutoSelectedModule_UseInstalledModulesForComputation() + { + $sModuleId = "any_module"; + $aModuleInfo = ['auto_select' => 'SetupInfo::ModuleIsSelected("a") && SetupInfo::ModuleIsSelected("b")']; + $aInstalledModules = ['a' => true, 'b' => true]; + $this->assertTrue($this->CallIsAutoSelectedModule($aInstalledModules, $sModuleId, $aModuleInfo)); + } + + private function CallIsAutoSelectedModule($aInstalledModules, $sModuleId, $aModuleInfo): bool + { + return $this->InvokeNonPublicMethod(InstallationChoicesToModuleConverter::class, 'IsAutoSelectedModule', InstallationChoicesToModuleConverter::GetInstance(), [$aInstalledModules, $sModuleId, $aModuleInfo]); + } + + public function testProcessInstallationChoices_Default() + { + $aRes = []; + $aInstallationDescription = $this->GivenInstallationChoiceDescription(); + + $this->CallGetModuleNamesFromInstallationChoices([], $aInstallationDescription, $aRes); + + $aExpected = [ + 'combodo-backoffice-darkmoon-theme' => true, + 'combodo-backoffice-fullmoon-high-contrast-theme' => true, + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme' => true, + 'combodo-backoffice-fullmoon-tritanopia-theme' => true, + 'itop-attachments' => true, + 'itop-backup' => true, + 'itop-config' => true, + 'itop-files-information' => true, + 'itop-profiles-itil' => true, + 'itop-structure' => true, + 'itop-themes-compat' => true, + 'itop-tickets' => true, + 'itop-welcome-itil' => true, + 'combodo-db-tools' => true, + 'itop-config-mgmt' => true, + 'itop-core-update' => true, + 'itop-hub-connector' => true, + 'itop-oauth-client' => true, + 'combodo-password-expiration' => true, + 'combodo-webhook-integration' => true, + 'combodo-my-account-user-info' => true, + 'authent-token' => true, + ]; + $this->assertEquals($aExpected, $aRes); + } + + public function testProcessInstallationChoices_NonItilChoices() + { + $aRes = []; + $aInstallationDescription = $this->GivenInstallationChoiceDescription(); + + $this->CallGetModuleNamesFromInstallationChoices($this->GivenNonItilChoices(), $aInstallationDescription, $aRes); + + $aExpected = [ + 'combodo-backoffice-darkmoon-theme' => true, + 'combodo-backoffice-fullmoon-high-contrast-theme' => true, + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme' => true, + 'combodo-backoffice-fullmoon-tritanopia-theme' => true, + 'itop-attachments' => true, + 'itop-backup' => true, + 'itop-config' => true, + 'itop-files-information' => true, + 'itop-profiles-itil' => true, + 'itop-structure' => true, + 'itop-themes-compat' => true, + 'itop-tickets' => true, + 'itop-welcome-itil' => true, + 'combodo-db-tools' => true, + 'itop-config-mgmt' => true, + 'itop-core-update' => true, + 'itop-hub-connector' => true, + 'itop-oauth-client' => true, + 'combodo-password-expiration' => true, + 'combodo-webhook-integration' => true, + 'combodo-my-account-user-info' => true, + 'authent-token' => true, + 'itop-datacenter-mgmt' => true, + 'itop-endusers-devices' => true, + 'itop-storage-mgmt' => true, + 'itop-virtualization-mgmt' => true, + 'itop-service-mgmt' => true, + 'itop-request-mgmt' => true, + 'itop-portal' => true, + 'itop-portal-base' => true, + 'itop-change-mgmt' => true, + 'itop-faq-light' => true, + 'itop-knownerror-mgmt' => true, + ]; + $this->assertEquals($aExpected, $aRes); + } + + public function testProcessInstallationChoices_ItilChoices() + { + $aRes = []; + $aInstallationDescription = $this->GivenInstallationChoiceDescription(); + + $this->CallGetModuleNamesFromInstallationChoices($this->GivenItilChoices(), $aInstallationDescription, $aRes); + + $aExpected = [ + 'combodo-backoffice-darkmoon-theme' => true, + 'combodo-backoffice-fullmoon-high-contrast-theme' => true, + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme' => true, + 'combodo-backoffice-fullmoon-tritanopia-theme' => true, + 'itop-attachments' => true, + 'itop-backup' => true, + 'itop-config' => true, + 'itop-files-information' => true, + 'itop-profiles-itil' => true, + 'itop-structure' => true, + 'itop-themes-compat' => true, + 'itop-tickets' => true, + 'itop-welcome-itil' => true, + 'combodo-db-tools' => true, + 'itop-config-mgmt' => true, + 'itop-core-update' => true, + 'itop-hub-connector' => true, + 'itop-oauth-client' => true, + 'combodo-password-expiration' => true, + 'combodo-webhook-integration' => true, + 'combodo-my-account-user-info' => true, + 'authent-token' => true, + 'itop-datacenter-mgmt' => true, + 'itop-endusers-devices' => true, + 'itop-storage-mgmt' => true, + 'itop-virtualization-mgmt' => true, + 'itop-service-mgmt' => true, + 'itop-portal' => true, + 'itop-portal-base' => true, + 'itop-request-mgmt-itil' => true, + 'itop-incident-mgmt-itil' => true, + 'itop-change-mgmt-itil' => true, + ]; + $this->assertEquals($aExpected, $aRes); + } + + private function CallGetModuleNamesFromInstallationChoices(array $aInstallationChoices, array $aInstallationDescription, array &$aModuleNames) + { + $this->InvokeNonPublicMethod( + InstallationChoicesToModuleConverter::class, + 'GetModuleNamesFromInstallationChoices', + InstallationChoicesToModuleConverter::GetInstance(), + [$aInstallationChoices, $aInstallationDescription, &$aModuleNames] + ); + } + + private function GivenInstallationChoiceDescription(): array + { + $oXMLParameters = new XMLParameters(__DIR__."/ressources/installation.xml"); + return $oXMLParameters->Get('steps', []); + } + + private function GivenAllModules(): array + { + return json_decode(file_get_contents(__DIR__.'/ressources/available_modules.json'), true); + } + + private function GivenNonItilChoices(): array + { + return [ + 'itop-config-mgmt-core', + 'itop-config-mgmt-datacenter', + 'itop-config-mgmt-end-user', + 'itop-config-mgmt-storage', + 'itop-config-mgmt-virtualization', + 'itop-service-mgmt-enterprise', + 'itop-ticket-mgmt-simple-ticket', + 'itop-ticket-mgmt-simple-ticket-enhanced-portal', + 'itop-change-mgmt-simple', + 'itop-kown-error-mgmt', + ]; + } + + private function GivenItilChoices(): array + { + return [ + 'itop-config-mgmt-datacenter', + 'itop-config-mgmt-end-user', + 'itop-config-mgmt-storage', + 'itop-config-mgmt-virtualization', + 'itop-service-mgmt-enterprise', + 'itop-ticket-mgmt-itil', + 'itop-ticket-mgmt-itil-user-request', + 'itop-ticket-mgmt-itil-incident', + 'itop-ticket-mgmt-itil-enhanced-portal', + 'itop-change-mgmt-itil', + 'itop-config-mgmt-core', + + ]; + } + + private function GivenModuleDiscoveryInit(): array + { + $aSearchDirs = [APPROOT.'datamodels/2.x']; + $this->SetNonPublicStaticProperty(ModuleDiscovery::class, 'm_aSearchDirs', $aSearchDirs); + $aAllModules = $this->GivenAllModules(); + $this->SetNonPublicStaticProperty(ModuleDiscovery::class, 'm_aModules', $aAllModules); + return $aSearchDirs; + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/analyze_installation_output.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/analyze_installation_output.json similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/ressources/analyze_installation_output.json rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/analyze_installation_output.json diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/available_modules.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/available_modules.json similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/ressources/available_modules.json rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/available_modules.json diff --git a/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation.xml b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation.xml new file mode 100644 index 000000000..e7270f896 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation.xml @@ -0,0 +1,230 @@ + + + + + Configuration Management options + The options below allow you to configure the type of elements that are to be managed inside iTop.]]> + /images/icons/icons8-apps-tab.svg + + + itop-config-mgmt-core + Configuration Management Core + All the base objects that are mandatory in the iTop CMDB: Organizations, Locations, Teams, Persons, etc. + + combodo-backoffice-darkmoon-theme + combodo-backoffice-fullmoon-high-contrast-theme + combodo-backoffice-fullmoon-protanopia-deuteranopia-theme + combodo-backoffice-fullmoon-tritanopia-theme + combodo-db-tools + combodo-password-expiration + combodo-webhook-integration + itop-attachments + itop-backup + itop-config + itop-config-mgmt + itop-core-update + itop-files-information + itop-hub-connector + itop-oauth-client + itop-profiles-itil + itop-structure + itop-themes-compat + itop-tickets + itop-welcome-itil + combodo-my-account-user-info + authent-token + + true + + + itop-config-mgmt-datacenter + Data Center Devices + Manage Data Center devices such as Racks, Enclosures, PDUs, etc. + + itop-datacenter-mgmt + + true + + + itop-config-mgmt-end-user + End-User Devices + Manage devices related to end-users: PCs, Phones, Tablets, etc. + + itop-endusers-devices + + true + + + itop-config-mgmt-storage + Storage Devices + Manage storage devices such as NAS, SAN Switches, Tape Libraries and Tapes, etc. + + itop-storage-mgmt + + true + + + itop-config-mgmt-virtualization + Virtualization + Manage Hypervisors, Virtual Machines and Farms. + + itop-virtualization-mgmt + + true + + + + + Service Management options + Select the choice that best describes the relationships between the services and the IT infrastructure in your IT environment.]]> + /images/icons/icons8-services.svg + + + itop-service-mgmt-enterprise + Service Management for Enterprises + Select this option if the IT delivers services based on a shared infrastructure. For example if different organizations within your company subscribe to services (like Mail and Print services) delivered by a single shared backend. + + itop-service-mgmt + + true + + + itop-service-mgmt-service-provider + Service Management for Service Providers + Select this option if the IT manages the infrastructure of independent customers. This is the most flexible model, since the services can be delivered with a mix of shared and customer specific infrastructure devices. + + itop-service-mgmt-provider + + + + + + Tickets Management options + Select the type of tickets you want to use in order to respond to user requests and incidents.]]> + /images/icons/icons8-discussion-forum.svg + + + itop-ticket-mgmt-simple-ticket + Simple Ticket Management + Select this option to use one single type of tickets for all kind of requests. + + itop-request-mgmt + + true + + + + itop-ticket-mgmt-simple-ticket-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-itil + ITIL Compliant Tickets Management + Select this option to have different types of ticket for managing user requests and incidents. Each type of ticket has a specific life cycle and specific fields + + + + itop-ticket-mgmt-itil-user-request + User Request Management + Manage User Request tickets in iTop + + itop-request-mgmt-itil + + + + itop-ticket-mgmt-itil-incident + Incident Management + Manage Incidents tickets in iTop + + itop-incident-mgmt-itil + + + + itop-ticket-mgmt-itil-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-none + No Tickets Management + Don't manage incidents or user requests in iTop + + + + + + + Change Management options + Select the type of tickets you want to use in order to manage changes to the IT infrastructure.]]> + /images/icons/icons8-change.svg + + + itop-change-mgmt-simple + Simple Change Management + Select this option to use one type of ticket for all kind of changes. + + itop-change-mgmt + + true + + + itop-change-mgmt-itil + ITIL Change Management + Select this option to use Normal/Routine/Emergency change tickets. + + itop-change-mgmt-itil + + + + itop-change-mgmt-none + No Change Management + Don't manage changes in iTop + + + + + + + Additional ITIL tickets + Pick from the list below the additional ITIL processes that are to be implemented in iTop.]]> + /images/icons/icons8-important-book.svg + + + + itop-kown-error-mgmt + Known Errors Management and FAQ + Select this option to track "Known Errors" and FAQs in iTop. + + itop-faq-light + itop-knownerror-mgmt + + + + itop-problem-mgmt + Problem Management + Select this option track "Problems" in iTop. + + itop-problem-mgmt + + + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation_choices_when_no_xml_file.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation_choices_when_no_xml_file.json new file mode 100644 index 000000000..2b455b857 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/installation_choices_when_no_xml_file.json @@ -0,0 +1 @@ +{"itop-config-mgmt":{"label":"Configuration+Management+customized+for+Combodo+IT(CMDB)","value":"2.7.0"},"itop-icalendar-action":{"label":"Calendar+Invitations","value":"1.1.0"},"itop-fence":{"label":"iTop+Fence","value":"1.1.2"},"authent-ldap":{"label":"User+authentication+based+on+LDAP","value":"3.2.1"},"itop-faq-light":{"label":"Frequently+Asked+Questions+Database","value":"3.2.1"},"authent-local":{"label":"User+authentication+based+on+the+local+DB","value":"3.2.1"},"combodo-custom-hyperlinks":{"label":"Hyperlinks+configurator","value":"1.1.3"},"authent-token":{"label":"User+authentication+by+token","value":"2.2.1"},"itop-service-mgmt":{"label":"Service+Management+Customized+for+Combodo+IT(services,+SLAs,+contracts)","value":"2.7.0"},"combodo-impersonate":{"label":"Impersonate+user+for+support","value":"1.2.1"},"combodo-hybridauth":{"label":"oAuth\/OpenID+authentication","value":"1.2.4"},"combodo-login-page":{"label":"Combodo+login+page","value":"2.1.0"},"itop-core-update":{"label":"iTop+Core+Update","value":"3.2.1"},"itop-communications":{"label":"Communications+to+the+Customers","value":"1.3.4"},"itsm-designer-connector":{"label":"ITSM+Designer+Connector","value":"1.8.3"},"authent-external":{"label":"External+user+authentication","value":"3.2.1"},"itop-object-copier":{"label":"Object+copier","value":"1.4.5"},"combodo-backoffice-compact-themes":{"label":"Backoffice:+compact+themes","value":"1.0.1"},"data-localizer":{"label":"Data+localizer","value":"1.3.4"},"combodo-support-portal":{"label":"Combodo+Support+Portal","value":"3.0.1"},"combodo-calendar-view":{"label":"Calendar+View","value":"2.2.1"},"combodo-email-synchro":{"label":"Tickets+synchronization+via+e-mail","value":"3.8.2"},"combodo-webhook-integration":{"label":"Webhook+integrations","value":"1.4.1"},"combodo-notify-on-expiration":{"label":"Notify+on+expiration","value":"1.0.4"},"combodo-db-tools":{"label":"Database+maintenance+tools","value":"3.2.1"},"precanned-replies":{"label":"Helpdesk+Precanned+Replies","value":"1.4.0"},"combodo-dokuwiki-portal-brick":{"label":"Docuwiki+brick+(Portal)","value":"1.2.0"},"itop-rh-mgmt":{"label":"Human+Resource+Management","value":"2.7.0"},"itop-request-mgmt":{"label":"User+request+management+(Service+Desk)","value":"2.7.0"},"customer-survey":{"label":"Customer+Survey","value":"2.5.5"},"itop-standard-email-synchro":{"label":"Ticket+Creation+from+Emails+(Standard)","value":"3.8.2"},"itop-system-information":{"label":"System+information","value":"1.2.6"},"itop-sales-mgmt":{"label":"Sales+Management","value":"2.7.0"},"combodo-password-expiration":{"label":"Password+Expiration+Enforcement","value":"1.0.0"},"combodo-workflow-graphical-view":{"label":"Workflow+graphical+view","value":"1.1.3"},"combodo-itsm-master":{"label":"Data+master+for+the+ITSM+Designer","value":"2.7.0"},"combodo-email-tickets":{"label":"Tickets+Creation+from+Emails+for+Combodo","value":"2.7.0"},"itop-training-mgmt":{"label":"Training+Management","value":"2.7.0"},"precanned-replies-pro":{"label":"Helpdesk+Precanned+Replies+Extension","value":"1.2.0"},"combodo-fulltext-search":{"label":"Enhanced+global+search","value":"2.0.0"},"itop-request-template":{"label":"Customized+Request+Forms","value":"2.3.6"},"itop-rest-data-push":{"label":"Data+push+(based+on+standard+REST+services)","value":"1.0.2"},"combodo-kpi-logger":{"label":"KPI+logger","value":"1.0.3"},"itop-incident-mgmt":{"label":"Incident+Management","value":"2.7.0"},"combodo-my-account-user-info":{"label":"User+info+for+MyAccount+module","value":"1.0.0"},"email-reply":{"label":"Send+Ticket+Log+Updates+by+Email","value":"1.4.5"},"itop-attachments":{"label":"Tickets+Attachments","value":"3.2.1"},"itop-log-mgmt":{"label":"iTop+Log+management","value":"2.0.8"},"itop-ui-copypaste":{"label":"CopyPaste+UI+Component","value":"1.0.0"}} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules.json similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules.json rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules.json diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules2.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules2.json similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules2.json rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules2.json diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules_simpleusecase.json b/tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules_simpleusecase.json similarity index 100% rename from tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules_simpleusecase.json rename to tests/php-unit-tests/unitary-tests/setup/moduleinstallation/ressources/priv_modules_simpleusecase.json