Compare commits

...

21 Commits

Author SHA1 Message Date
Timothee
8aac64cc19 N°8864 Added tests 2026-01-05 10:34:42 +01:00
Timothee
f668d94bd9 N°8864 Style fix 2025-12-23 16:20:33 +01:00
Timothee
00e071228e N°8864 List forced uninstall 2025-12-23 16:20:33 +01:00
Timothee
19b7f851f7 N°8864 List extensions added or removed in recap 2025-12-23 16:20:33 +01:00
Timothee
f9a1b444ab N°9010 Setup wizard : manage multiple level extension choice 2025-12-23 16:05:23 +01:00
odain
3b38dac8c6 Merge branch 'develop' into feature/uninstallation 2025-12-19 18:45:34 +01:00
odain
b967fb7f20 Merge branch 'develop' into feature/uninstallation 2025-12-19 16:17:12 +01:00
odain
195038c941 Merge branch 'develop' into feature/uninstallation 2025-12-19 15:48:56 +01:00
odain
9a8d87e2b5 Merge branch 'develop' into feature/uninstallation 2025-12-18 14:02:45 +01:00
odain
385302c44c ci: fix nightly 2025-12-08 10:51:28 +01:00
odain
b563f113d0 add combodo-data-feature-removal 2025-12-08 10:38:30 +01:00
odain
715d9d3b1c less logs during setup 2025-12-05 17:37:09 +01:00
odain
7a6c2bc6a4 ci: fix with package 2025-12-05 17:36:57 +01:00
odain
8919184ef9 ci: adapt test to run also with itop packages 2025-12-05 11:36:19 +01:00
odain
7bfaebe4db ci: fix merge and issues with LoadChoicesFromDatabase 2025-12-04 18:40:21 +01:00
odain
8e10cf9b72 N°8760 : fix broken setup
ci: fix merge
2025-12-04 18:28:47 +01:00
odain
9f3d7d2c36 N°8760: revert dry removal audit and use real file deletion again
code formatting
2025-12-04 17:19:18 +01:00
odain
ae980e365d N°8760 :fix ModuleInstallation db query + refactor query in ModuleInstallationService
extensionmap cleanup

fix setup broken
2025-12-04 17:19:18 +01:00
odain
a2b01b3ed4 N°8760 - setupaudit and dry removal API review with Romain - avoid file deletion
N°8760 - be able to simulate extension removal by oerriding GetExtensionMap

be able to simulate SetupAudit errors in Setups for integration tests

fix rebase
2025-12-04 17:19:18 +01:00
odain
9cdc707bc5 N°8760 - Audit uninstall of extensions that declare classes - first prototype
N°8760 - Audit uninstall of extensions that declare classes - be able to trace DM classes created_in

N°8760 - be able to test with additional extensions installed in test SDK

N°8760 - provide a service dedicated to extension removal

N°8760 - compute all rules by default

add comment

adapt audit to both extension and mtp
2025-12-04 17:19:18 +01:00
odain
76178c16b8 N°8760: code legacy refactoring around module computation (AnalyzeInstallation)
AnalyzeInstallation refactoring

fix ci call to ModuleInstallationService

ci: fix code style

get rid of itop_version

refactoring: make code simpler

get rid of unused name_db

refactoring setup: rename version_db by installed_version

refactoring setup: rename version_code by available_version

code cleanup: avoid useless runtimeenv instanciation
2025-12-04 17:19:18 +01:00
46 changed files with 14209 additions and 349 deletions

View File

@@ -462,6 +462,19 @@ abstract class MetaModel
return call_user_func([$sClass, 'GetClassDescription'], $sClass);
}
/**
* @param string $sClass
*
* @return string
* @throws \CoreException
*/
final public static function GetCreatedIn($sClass)
{
self::_check_subclass($sClass);
return self::$m_aClassParams[$sClass]["created_in"] ?? "";
}
/**
* @param string $sClass
*
@@ -3145,6 +3158,7 @@ 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",

View File

@@ -135,7 +135,7 @@ class RunTimeEnvironmentCoreUpdater extends RunTimeEnvironment
$aAvailableModules[$oModule->GetName()] = $oModule;
}
// TODO check the auto-selected modules here
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if ($oExtension->bMarkedAsChosen) {
foreach ($oExtension->aModules as $sModuleName) {
if (!isset($aRet[$sModuleName]) && isset($aAvailableModules[$sModuleName])) {

View File

@@ -186,9 +186,7 @@ function collect_configuration()
// iTop modules
$oConfig = MetaModel::GetConfig();
$sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->Get('db_subname')."priv_module_install");
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$aInstalledModules = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install WHERE installed = '".$sLatestInstallationDate."' AND parent_id != 0");
$aInstalledModules = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
foreach ($aInstalledModules as $aDBInfo) {
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];

View File

@@ -26,23 +26,12 @@ use Composer\Semver\VersionParser;
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@@ -320,24 +309,6 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
@@ -354,9 +325,7 @@ class InstalledVersions
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
@@ -364,14 +333,11 @@ class InstalledVersions
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $required;
self::$installedIsLocalDir = true;
$copiedLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}

View File

@@ -3,7 +3,7 @@
'name' => 'combodo/itop',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'combodo/itop' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19d062aa830b6d6c7d17ac4046fc9ee2c5e3fab1',
'reference' => '469afdb2f9aea1b6e078a2a5bb12f09a969d60e0',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@@ -0,0 +1,130 @@
<?php
require_once __DIR__.'/ModuleInstallationService.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
private ?array $aSelectInstall = null;
protected function __construct()
{
}
final public static function GetInstance(): AnalyzeInstallation
{
if (!isset(self::$oInstance)) {
self::$oInstance = new AnalyzeInstallation();
}
return self::$oInstance;
}
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* Analyzes the current installation and the possibilities
*
* @param null|Config $oConfig Defines the target environment (DB)
* @param mixed $modulesPath Either a single string or an array of absolute paths
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'uninstall' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'label' => ...
* 'dependencies' => array(<module1>, <module2>, ...)
* 'visible' => true | false
* )
* )
* @throws \Exception
*/
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'installed_version' => '',
'available_version' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
if (! is_null($this->aAvailableModules)) {
//test only
$aAvailableModules = $this->aAvailableModules;
} else {
$aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
$aModuleInfo['installed_version'] = '';
$aModuleInfo['available_version'] = $sModuleVersion;
if ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
$aCurrentlyInstalledModules = ModuleInstallationService::GetInstance()->ReadComputeInstalledModules($oConfig);
// Adjust the list of proposed modules
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
continue;
}
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['mandatory']) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
}
}

View File

@@ -0,0 +1,184 @@
<?php
class ModuleInstallationService
{
private static ModuleInstallationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationService();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationService $oInstance): void
{
static::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
/**
* @param \Config|null $oConfig
* @return array
*/
public function ReadComputeInstalledModules(?Config $oConfig): array
{
$aSelectInstall = [];
try {
$aSelectInstall = $this->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
}
return $this->ComputeInstalledModules($aSelectInstall);
}
/**
* @param \Config|null $oConfig
* @return array
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
*/
public function ReadFromDB(?Config $oConfig): array
{
if (is_null($oConfig)) {
return [];
}
if (! is_null($this->aSelectInstall)) {
//test only
return $this->aSelectInstall;
}
CMDBSource::InitFromConfig($oConfig);
//read db module installations
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$iRootId = CMDBSource::QueryToScalar("SELECT max(parent_id) FROM $tableWithPrefix");
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$sSQL = <<<SQL
SELECT * FROM $tableWithPrefix
WHERE
parent_id='$iRootId'
OR id='$iRootId'
SQL;
return CMDBSource::QueryToArray($sSQL);
}
private function GetTableWithPrefix(Config $oConfig)
{
$sPrefix = $oConfig->Get('db_subname');
if (utils::IsNullOrEmptyString($sPrefix)) {
return "priv_module_install";
}
return "{$sPrefix}priv_module_install";
}
/**
* @param \Config $oConfig
*
* @return array|false
*/
public function GetApplicationVersion(Config $oConfig)
{
try {
CMDBSource::InitFromConfig($oConfig);
$tableWithPrefix = $this->GetTableWithPrefix($oConfig);
$sSQLQuery = "SELECT * FROM $tableWithPrefix";
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
} catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
$this->log_error('Exception '.$e->getMessage());
return false;
}
$aResult = [];
// Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version
foreach ($aSelectInstall as $aInstall) {
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
if ($aInstall['name'] == DATAMODEL_MODULE) {
$aResult['datamodel_version'] = $sModuleVersion;
$aComments = json_decode($aInstall['comment'], true);
if (is_array($aComments)) {
$aResult = array_merge($aResult, $aComments);
}
} else {
$aResult['product_name'] = $aInstall['name'];
$aResult['product_version'] = $sModuleVersion;
}
}
}
if (!array_key_exists('datamodel_version', $aResult)) {
// Versions prior to 2.0 did not record the version of the datamodel
// so assume that the datamodel version is equal to the application version
$aResult['datamodel_version'] = $aResult['product_version'];
}
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return empty($aResult) ? false : $aResult;
}
private function ComputeInstalledModules(array $aSelectInstall): array
{
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
//module installation datetime is mostly the same for all modules
//unless there was issue recording things in DB
$sFirstDatetime = null;
$iFirstTime = -1;
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$sDatetime = $aInstall['installed'];
if (is_null($sFirstDatetime)) {
$sFirstDatetime = $sDatetime;
$iFirstTime = strtotime($sDatetime);
$iInstalled = $iFirstTime;
} elseif ($sDatetime === $sFirstDatetime) {
$iInstalled = $iFirstTime;
} else {
$sDatetime = $aInstall['installed'];
$iInstalled = strtotime($sDatetime);
}
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$aInstallByModule[ROOT_MODULE] = [
'installed_version' => $sModuleVersion,
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
} else {
$aInstallByModule[$sModuleName] = [
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
}
}
return $aInstallByModule;
}
}

View File

@@ -477,7 +477,7 @@ class MFCompiler
$sClass = $oClass->getAttribute("id");
$aAllClasses[] = $sClass;
try {
$sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
$sCompiledCode .= $this->CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
} catch (DOMFormatException $e) {
$sMessage = "Failed to process class '$sClass', ";
if (!empty($sModuleRootDir)) {
@@ -1189,6 +1189,7 @@ EOF
/**
* @param \MFElement $oClass
* @param string $sModuleName
* @param string $sTempTargetDir
* @param string $sFinalTargetDir
* @param string $sModuleRelativeDir
@@ -1196,7 +1197,7 @@ EOF
* @return string
* @throws \DOMFormatException
*/
protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
{
$sClass = $oClass->getAttribute('id');
$oProperties = $oClass->GetUniqueElement('properties');
@@ -1209,6 +1210,7 @@ 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';
}

View File

@@ -148,32 +148,38 @@ class iTopExtensionsMap
{
/**
* The list of all discovered extensions
* @param string $sFromEnvironment The environment to scan
* @param bool $bNormailizeOldExtension true to "magically" convert some well-known old extensions (i.e. a set of modules) to the new iTopExtension format
* @return void
* @var array $aExtensions
*/
protected $aExtensions;
/**
* The list of all currently installed extensions
* @var array|null
* @var array $aInstalledExtensions
*/
protected ?array $aInstalledExtensions = null;
protected array $aInstalledExtensions;
protected array $aExtensionsByCode;
/**
* The list of directories browsed using the ReadDir method when building the map
* @var string[]
*/
protected $aScannedDirs;
/**
* The list of all discovered extensions
* @param string $sFromEnvironment The environment to scan
* @param bool $bNormailizeOldExtension true to "magically" convert some well-known old extensions (i.e. a set of modules) to the new iTopExtension format
* @return void
*/
public function __construct($sFromEnvironment = 'production', $aExtraDirs = [])
{
$this->aExtensions = [];
$this->aExtensionsByCode = [];
$this->aScannedDirs = [];
$this->ScanDisk($sFromEnvironment);
foreach ($aExtraDirs as $sDir) {
$this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE);
}
$this->CheckDependencies($sFromEnvironment);
$this->CheckDependencies();
}
/**
@@ -184,8 +190,10 @@ class iTopExtensionsMap
*/
protected function ScanDisk($sEnvironment)
{
if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x') && !$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) {
if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) {
//no installation xml found in 2.x: let's read all extensions in 2.x first
if (!$this->ReadDir(APPROOT.'/datamodels/2.x', iTopExtension::SOURCE_WIZARD)) {
//nothing found in 2.x : fallback read in 1.x (flat structure)
$this->ReadDir(APPROOT.'/datamodels/1.x', iTopExtension::SOURCE_WIZARD);
}
}
@@ -261,6 +269,7 @@ class iTopExtensionsMap
// This "new" extension is "newer" than the previous one, let's replace the previous one
unset($this->aExtensions[$key]);
$this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension;
$this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension;
return;
} else {
// This "new" extension is not "newer" than the previous one, let's ignore it
@@ -270,6 +279,22 @@ class iTopExtensionsMap
}
// Finally it's not a duplicate, let's add it to the list
$this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension;
$this->aExtensionsByCode[$oNewExtension->sCode] = $oNewExtension;
}
public function RemoveExtension(string $sCode): void
{
$oExtension = $this->GetFromExtensionCode($sCode);
if (is_null($oExtension)) {
\IssueLog::Error(__METHOD__.": cannot find extension to remove", null, [$sCode]);
return;
}
\IssueLog::Debug(__METHOD__.": remove extension from map", null, [$oExtension->sCode => $oExtension->sSourceDir]);
unset($this->aExtensions[$oExtension->sCode.'/'.$oExtension->sVersion]);
unset($this->aExtensionsByCode[$sCode]);
}
/**
@@ -280,13 +305,31 @@ class iTopExtensionsMap
*/
public function GetFromExtensionCode(string $sExtensionCode): ?iTopExtension
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode === $sExtensionCode) {
return $oExtension;
return $this->aExtensionsByCode[$sExtensionCode] ?? null;
}
/*public function GetMissingExtensions(array $aSelectedExtensions)
{
\SetupLog::Info(__METHOD__, null, ['selected' => $aSelectedExtensions]);
$aExtensionsFromDb = array_keys($this->aExtensionsByCode);
sort($aExtensionsFromDb);
\SetupLog::Info(__METHOD__, null, ['found' => $aExtensionsFromDb]);
$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;
} else {
\SetupLog::Info(__METHOD__." MISSING $sExtensionCode");
}
}
return null;
}
\SetupLog::Info(__METHOD__, null, $aRes);
return $aRes;
}*/
/**
* Read (recursively) a directory to find if it contains extensions (or modules)
@@ -346,19 +389,15 @@ class iTopExtensionsMap
// to this extension
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID];
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if ($sModuleVersion == '') {
// Provide a default module version since version is mandatory when recording ExtensionInstallation
$sModuleVersion = '0.0.1';
}
$aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]['uninstallable'] ??= 'yes';
if (($sParentExtensionId !== null) && (array_key_exists($sParentExtensionId, $this->aExtensions)) && ($this->aExtensions[$sParentExtensionId] instanceof iTopExtension)) {
// Already inside an extension, let's add this module the list of modules belonging to this extension
$this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName;
$this->aExtensions[$sParentExtensionId]->aModuleVersion[$sModuleName] = $sModuleVersion;
$this->aExtensions[$sParentExtensionId]->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG];
} else {
// Not already inside a folder containing an 'extension.xml' file
$oExtension = null;
if ($sParentExtensionId !== null) {
$oExtension = $this->aExtensions[$sParentExtensionId] ?? null;
}
if (is_null($oExtension)) {
// Not already inside an folder containing an 'extension.xml' file
// Ignore non-visible modules and auto-select ones, since these are never prompted
// as a choice to the end-user
@@ -382,6 +421,13 @@ class iTopExtensionsMap
$oExtension->sSourceDir = $sSearchDir;
$oExtension->bVisible = $bVisible;
$this->AddExtension($oExtension);
} else {
$oExtension->aModules[] = $sModuleName;
$oExtension->aModuleVersion[$sModuleName] = $sModuleVersion;
$oExtension->aModuleInfo[$sModuleName] = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG];
$this->aExtensions[$sParentExtensionId] = $oExtension;
$this->aExtensionsByCode[$oExtension->sCode] = $oExtension;
}
closedir($hDir);
@@ -403,10 +449,9 @@ class iTopExtensionsMap
/**
* Check if some extension contains a module with missing dependencies...
* If so, populate the aMissingDepenencies array
* @param string $sFromEnvironment
* @return void
*/
protected function CheckDependencies($sFromEnvironment)
protected function CheckDependencies()
{
$aSearchDirs = [];
@@ -418,7 +463,7 @@ class iTopExtensionsMap
$aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs);
try {
$aAllModules = ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
ModuleDiscovery::GetAvailableModules($aSearchDirs, true);
} catch (MissingDependencyException $e) {
// Some modules have missing dependencies
// Let's check what is the impact at the "extensions" level
@@ -466,11 +511,17 @@ class iTopExtensionsMap
*/
public function MarkAsChosen($sExtensionCode, $bMark = true)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
$oExtension->bMarkedAsChosen = $bMark;
break;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->bMarkedAsChosen = $bMark;
}
}
public function MarkAsUninstallable($sExtensionCode, $bMark = true)
{
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->bUninstallable = $bMark;
}
}
@@ -481,11 +532,11 @@ class iTopExtensionsMap
*/
public function IsMarkedAsChosen($sExtensionCode)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
return $oExtension->bMarkedAsChosen;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
return $oExtension->bMarkedAsChosen;
}
return false;
}
@@ -497,11 +548,9 @@ class iTopExtensionsMap
*/
protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion)
{
foreach ($this->aExtensions as $oExtension) {
if ($oExtension->sCode == $sExtensionCode) {
$oExtension->sInstalledVersion = $sInstalledVersion;
break;
}
$oExtension = $this->GetFromExtensionCode($sExtensionCode);
if (!is_null($oExtension)) {
$oExtension->sInstalledVersion = $sInstalledVersion;
}
}
@@ -527,7 +576,12 @@ class iTopExtensionsMap
*/
public function LoadChoicesFromDatabase(Config $oConfig)
{
foreach ($this->LoadInstalledExtensionsFromDatabase($oConfig) as $oExtension) {
$aLoadInstalledExtensionsFromDatabase = $this->LoadInstalledExtensionsFromDatabase($oConfig);
if (false === $aLoadInstalledExtensionsFromDatabase) {
return false;
}
foreach ($aLoadInstalledExtensionsFromDatabase as $oExtension) {
$this->MarkAsChosen($oExtension->sCode);
$this->SetInstalledVersion($oExtension->sCode, $oExtension->sVersion);
}
@@ -585,8 +639,6 @@ class iTopExtensionsMap
*/
public function ModuleIsChosenAsPartOfAnExtension($sModuleNameToFind, $sInSourceOnly = iTopExtension::SOURCE_REMOTE)
{
$bChosen = false;
foreach ($this->GetAllExtensions() as $oExtension) {
if (($oExtension->sSource == $sInSourceOnly) &&
($oExtension->bMarkedAsChosen == true) &&

View File

@@ -0,0 +1,97 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
use RunTimeEnvironment;
use SetupUtils;
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
*
* @param string $sEnvironment (e.g. 'test')
* @param bool $bAutoCommit (make the target environment directly, or build a temporary one)
*/
public function __construct($sEnvironment = self::DRY_REMOVAL_AUDIT_ENV, $bAutoCommit = true)
{
parent::__construct($sEnvironment, $bAutoCommit);
$this->aExtensionsByCode = [];
}
/**
* @param string $sSourceEnv
* @param array $aExtensionCodesToRemove
*
* @return void
* @throws \Exception
*/
public function Prepare(string $sSourceEnv, array $aExtensionCodesToRemove)
{
$sEnv = $this->sFinalEnv;
$this->aExtensionsByCode = $aExtensionCodesToRemove;
//SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules");
$this->Cleanup();
SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules");
if (count($aExtensionCodesToRemove) > 0) {
$this->RemoveExtensionsLocally($aExtensionCodesToRemove);
}
$oDryRemovalConfig = clone(MetaModel::GetConfig());
$oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv);
$this->WriteConfigFileSafe($oDryRemovalConfig);
}
private function RemoveExtensionsLocally(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]);
}
}
}
public function Cleanup()
{
$sEnv = $this->sFinalEnv;
SetupUtils::rrmdir(APPROOT."/data/$sEnv-modules");
SetupUtils::rrmdir(APPROOT."/data/cache-$sEnv");
SetupUtils::rrmdir(APPROOT."/env-$sEnv");
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();
}*/
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use CoreException;
use Exception;
class ModelReflectionSerializer
{
private static ModelReflectionSerializer $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModelReflectionSerializer
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModelReflectionSerializer();
}
return self::$oInstance;
}
final public static function SetInstance(?ModelReflectionSerializer $oInstance): void
{
self::$oInstance = $oInstance;
}
public function GetModelFromEnvironment(string $sEnv): array
{
\IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(\MetaModel::GetConfig()->Get('php_path'));
$sOutput = "";
$iRes = 0;
exec(sprintf("$sPHPExec %s/get_model_reflection.php --env='%s'", __DIR__, $sEnv), $sOutput, $iRes);
if ($iRes != 0) {
\IssueLog::Error("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
throw new CoreException("Cannot get classes");
}
$aClasses = json_decode($sOutput[0] ?? null, true);
if (false === $aClasses) {
\IssueLog::Error("Invalid JSON", null, ["output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
\IssueLog::Error("not an array", null, ["classes" => $aClasses]);
throw new Exception("cannot get classes");
}
return $aClasses;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use DBObjectSearch;
use DBObjectSet;
use MetaModel;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class SetupAudit
{
//file used when present to trigger audit exception when testing specific setups
public const GETISSUE_ERROR_MSG_FILE_FORTESTONLY = '.setup_audit_error_msg.txt';
private string $sEnvBeforeExtensionRemoval;
private string $sEnvAfterExtensionRemoval;
private array $aClassesBeforeRemoval;
private array $aClassesAfterRemoval;
private array $aRemovedClasses;
private array $aFinalClassesRemoved;
public function __construct(string $sEnvBeforeExtensionRemoval, string $sEnvAfterExtensionRemoval = DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV)
{
$this->sEnvBeforeExtensionRemoval = $sEnvBeforeExtensionRemoval;
$this->sEnvAfterExtensionRemoval = $sEnvAfterExtensionRemoval;
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvBeforeExtensionRemoval) {
$this->aClassesBeforeRemoval = MetaModel::GetClasses();
} else {
$this->aClassesBeforeRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBeforeExtensionRemoval);
}
if ($sCurrentEnvt === $this->sEnvAfterExtensionRemoval) {
$this->aClassesAfterRemoval = MetaModel::GetClasses();
} else {
$this->aClassesAfterRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfterExtensionRemoval);
}
$this->aRemovedClasses = [];
$this->aFinalClassesRemoved = [];
}
/*public function SetSelectedExtensions(Config $oConfig, array $aSelectedExtensions)
{
$oExtensionsMap = new \iTopExtensionsMap();
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
sort($aSelectedExtensions);
$this->aExtensionToRemove = $oExtensionsMap->GetMissingExtensions($aSelectedExtensions);
sort($this->aExtensionToRemove);
\SetupLog::Info(__METHOD__, null, ['aExtensionToRemove' => $this->aExtensionToRemove]);
}*/
public function GetRemovedClasses(): array
{
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBeforeRemoval) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfterRemoval) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBeforeRemoval, $this->aClassesAfterRemoval);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
foreach ($aClasses as $i => $sClass) {
$this->aRemovedClasses[] = $sClass;
}
}
return $this->aRemovedClasses;
}
/** test only: return file path that force audit error being raised
*
* @return string
*/
public static function GetErrorMessageFilePathForTestOnly(): string
{
return APPROOT."/data/".self::GETISSUE_ERROR_MSG_FILE_FORTESTONLY;
}
public function GetIssues(bool $bThrowExceptionAtFirstIssue = false): array
{
$sErrorMessageFilePath = self::GetErrorMessageFilePathForTestOnly();
if ($bThrowExceptionAtFirstIssue && is_file($sErrorMessageFilePath)) {
$sMsg = file_get_contents($sErrorMessageFilePath);
throw new \Exception($sMsg);
}
$this->aFinalClassesRemoved = [];
foreach ($this->GetRemovedClasses() as $sClass) {
if (MetaModel::IsAbstract($sClass)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesRemoved[$sClass] = $iCount;
if ($bThrowExceptionAtFirstIssue && $iCount > 0) {
//setup envt: should raise issue ASAP
throw new \Exception($sClass);
}
}
}
return $this->aFinalClassesRemoved;
}
private function Count($sClass): int
{
$oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
return $oSet->Count();
}
}

View File

@@ -0,0 +1,41 @@
<?php
require_once(dirname(__DIR__, 2).'/approot.inc.php');
require_once(APPROOT.'application/application.inc.php');
$sEnv = null;
if (isset($argv)) {
foreach ($argv as $iArg => $sArg) {
if (preg_match('/^--env=(.*)$/', $sArg, $aMatches)) {
$sEnv = $aMatches[1];
}
}
}
if (is_null($sEnv)) {
echo "No environment provided (--env) to read datamodel.";
exit(1);
}
$sConfFile = utils::GetConfigFilePath($sEnv);
try {
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
} catch (\Throwable $e) {
echo $e->getMessage();
echo $e->getTraceAsString();
\SetupLog::Error(
"Cannot read model from provided environment",
null,
[
'env' => $sEnv,
'error' => $e->getMessage(),
'stack' => $e->getTraceAsString(),
]
);
echo "Cannot read model from provided environment";
exit(1);
}
$aClasses = MetaModel::GetClasses();
echo json_encode($aClasses);

View File

@@ -120,10 +120,6 @@ class ModuleDiscovery
if (is_null($aArgs) || ! is_array($aArgs)) {
throw new ModuleFileReaderException("Error parsing module file args", 0, null, $sFilePath);
}
if (!array_key_exists('itop_version', $aArgs)) {
// Assume 1.0.2
$aArgs['itop_version'] = '1.0.2';
}
foreach (array_keys(self::$m_aModuleArgs) as $sArgName) {
if (!array_key_exists($sArgName, $aArgs)) {
throw new Exception("Module '$sId': missing argument '$sArgName'");

View File

@@ -32,6 +32,7 @@ require_once APPROOT."setup/modulediscovery.class.inc.php";
require_once APPROOT.'setup/modelfactory.class.inc.php';
require_once APPROOT.'setup/compiler.class.inc.php';
require_once APPROOT.'setup/extensionsmap.class.inc.php';
require_once APPROOT.'setup/AnalyzeInstallation.php';
define('MODULE_ACTION_OPTIONAL', 1);
define('MODULE_ACTION_MANDATORY', 2);
@@ -61,7 +62,23 @@ class RunTimeEnvironment
* Extensions map of the source environment
* @var iTopExtensionsMap
*/
protected $oExtensionsMap;
protected ?iTopExtensionsMap $oExtensionsMap;
protected function GetExtensionMap(): ?iTopExtensionsMap
{
return $this->oExtensionsMap;
}
public function InitExtensionMap($aExtraDirs, $oSourceConfig)
{
// Actually read the modules available for the target environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen alll the "remote" modules present in the
// target environment (data/<target-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
}
/**
* Toolset for building a run-time environment
@@ -145,12 +162,12 @@ class RunTimeEnvironment
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'version_db' => ... (could be empty in case of a fresh install)
* 'version_code => ...
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'version_db' => ...
* 'version_code' => ...
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
@@ -168,137 +185,7 @@ class RunTimeEnvironment
*/
public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'version_db' => '',
'name_db' => '',
'version_code' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
$aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
foreach ($aModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if ($sModuleName == '') {
throw new Exception("Missing name for the module: '$sModuleId'");
}
if ($sModuleVersion == '') {
// The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not)
//throw new Exception("Missing version for the module: '$sModuleId'");
$sModuleVersion = '1.0.0';
}
$sModuleAppVersion = $aModuleInfo['itop_version'];
$aModuleInfo['version_db'] = '';
$aModuleInfo['version_code'] = $sModuleVersion;
if (!in_array($sModuleAppVersion, ['1.0.0', '1.0.1', '1.0.2'])) {
// This module is NOT compatible with the current version
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is not compatible with the current version of the application',
];
} elseif ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
try {
$aSelectInstall = [];
if (! is_null($oConfig)) {
CMDBSource::InitFromConfig($oConfig);
$aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install");
}
} catch (MySQLException $e) {
// No database or erroneous information
}
// Build the list of installed module (get the latest installation)
//
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
$iRootId = 0;
foreach ($aSelectInstall as $aInstall) {
if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) {
// Root module, what is its ID ?
$iId = (int) $aInstall['id'];
if ($iId > $iRootId) {
$iRootId = $iId;
}
}
}
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$iInstalled = strtotime($aInstall['installed']);
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$sModuleName = ROOT_MODULE;
} elseif ($aInstall['parent_id'] != $iRootId) {
// Skip all modules belonging to previous installations
continue;
}
if (array_key_exists($sModuleName, $aInstallByModule)) {
if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) {
continue;
}
}
if ($aInstall['parent_id'] == 0) {
$aRes[$sModuleName]['version_db'] = $sModuleVersion;
$aRes[$sModuleName]['name_db'] = $aInstall['name'];
}
$aInstallByModule[$sModuleName]['installed'] = $iInstalled;
$aInstallByModule[$sModuleName]['version'] = $sModuleVersion;
}
// Adjust the list of proposed modules
//
foreach ($aInstallByModule as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
continue;
} // Skip the main module
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['version_db'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
return AnalyzeInstallation::GetInstance()->AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
@@ -344,6 +231,7 @@ class RunTimeEnvironment
*/
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)");
@@ -360,30 +248,29 @@ class RunTimeEnvironment
$aExtraDirs = $this->GetExtraDirsToScan($aDirsToCompile);
$aDirsToCompile = array_merge($aDirsToCompile, $aExtraDirs);
$aRet = [];
// Determine the installed modules and extensions
//
$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
$oSourceEnv = new RunTimeEnvironment($sSourceEnv);
$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
$aAvailableModules = $this->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
// Actually read the modules available for the target environment,
// but get the selection from the source environment and finally
// mark as (automatically) chosen alll the "remote" modules present in the
// mark as (automatically) chosen all the "remote" modules present in the
// target environment (data/<target-env>-modules)
// The actual choices will be recorded by RecordInstallation below
$this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv, $aExtraDirs);
$this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig);
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
$this->InitExtensionMap($aExtraDirs, $oSourceConfig);
$this->GetExtensionMap()->LoadChoicesFromDatabase($oSourceConfig);
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if ($this->IsExtensionSelected($oExtension)) {
$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
$this->GetExtensionMap()->MarkAsChosen($oExtension->sCode);
}
}
// Do load the required modules
//
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
$aRet = [];
$aRet[$oDictModule->GetName()] = $oDictModule;
$oFactory = new ModelFactory($aDirsToCompile);
@@ -401,10 +288,9 @@ class RunTimeEnvironment
$aModules = $oFactory->FindModules();
foreach ($aModules as $oModule) {
$sModule = $oModule->GetName();
$sModuleRootDir = $oModule->GetRootDir();
$bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
$bIsExtra = $this->GetExtensionMap()->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
if (array_key_exists($sModule, $aAvailableModules)) {
if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
$aRet[$oModule->GetName()] = $oModule;
}
}
@@ -648,7 +534,7 @@ class RunTimeEnvironment
$oInstallRec->Set('comment', json_encode($aData));
$oInstallRec->Set('parent_id', 0); // root module
$oInstallRec->Set('installed', $iInstallationTime);
$iMainItopRecord = $oInstallRec->DBInsertNoReload();
$oInstallRec->DBInsertNoReload();
// Record main installation
$oInstallRec = new ModuleInstallation();
@@ -661,7 +547,6 @@ class RunTimeEnvironment
// Record installed modules and extensions
//
$aAvailableExtensions = [];
$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
foreach ($aSelectedModuleCodes as $sModuleId) {
if (!array_key_exists($sModuleId, $aAvailableModules)) {
@@ -669,7 +554,7 @@ class RunTimeEnvironment
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['version_code'];
$sVersion = $aModuleData['available_version'];
$sUninstallable = $aModuleData['uninstallable'] ?? 'yes';
$aComments = [];
$aComments[] = $sShortComment;
@@ -702,16 +587,17 @@ class RunTimeEnvironment
$oInstallRec->DBInsertNoReload();
}
if ($this->oExtensionsMap) {
if ($this->GetExtensionMap()) {
// Mark as chosen the selected extensions code passed to us
// Note: some other extensions may already be marked as chosen
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
foreach ($this->GetExtensionMap()->GetAllExtensions() as $oExtension) {
if (in_array($oExtension->sCode, $aSelectedExtensionCodes)) {
$this->oExtensionsMap->MarkAsChosen($oExtension->sCode);
$this->GetExtensionMap()->MarkAsChosen($oExtension->sCode);
}
}
foreach ($this->oExtensionsMap->GetChoices() as $oExtension) {
foreach ($this->GetExtensionMap()->GetChoices() as $oExtension) {
$oInstallRec = new ExtensionInstallation();
$oInstallRec->Set('code', $oExtension->sCode);
$oInstallRec->Set('label', $oExtension->sLabel);
@@ -739,9 +625,7 @@ class RunTimeEnvironment
public function GetApplicationVersion(Config $oConfig)
{
try {
CMDBSource::InitFromConfig($oConfig);
$sSQLQuery = "SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install";
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
$aSelectInstall = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
} catch (MySQLException $e) {
// No database or erroneous information
$this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name'));
@@ -975,7 +859,7 @@ class RunTimeEnvironment
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']];
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
}
}
@@ -1039,7 +923,7 @@ class RunTimeEnvironment
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (in_array($sModuleId, $aSelectedModules)) {
if ($aModule['version_db'] != '') {
if ($aModule['installed_version'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);

View File

@@ -1555,17 +1555,8 @@ JS
return $sHtml;
}
/**
* @param \WizardController $oWizard
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
*
* @return array
* @throws Exception
*/
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
public static function GetConfig($oWizard)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = new Config();
$sSourceDir = $oWizard->GetParameter('source_dir', '');
@@ -1580,7 +1571,25 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = $sRelativeSourceDir;
$oConfig->UpdateFromParams($aParamValues, null);
$aDirsToScan = [$sSourceDir];
return $oConfig;
}
/**
* @param \WizardController $oWizard
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if ommitted
*
* @return array
* @throws Exception
*/
public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
$oConfig = self::GetConfig($oWizard);
$aDirsToScan = [$oWizard->GetParameter('source_dir', '')];
if (is_dir(APPROOT.'extensions')) {
$aDirsToScan[] = APPROOT.'extensions';

View File

@@ -50,6 +50,7 @@ require_once(APPROOT.'setup/applicationinstaller.class.inc.php');
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'setup/extensionsmap.class.inc.php');
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
/**
* First step of the iTop Installation Wizard: Welcome screen, requirements
@@ -1367,6 +1368,52 @@ class WizStepModulesChoice extends WizardStep
return ['WizStepModulesChoice', 'WizStepSummary'];
}
public function GetAddedAndRemovedExtensions($aSelectedExtensions)
{
$aExtensionsAdded = [];
$aExtensionsRemoved = [];
$aExtensionsNotUninstallable = [];
foreach ($this->oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
/* @var \iTopExtension $oExtension */
$bSelected = in_array($oExtension->sCode, $aSelectedExtensions);
if ($oExtension->bInstalled && !$bSelected) {
$aExtensionsRemoved[$oExtension->sCode] = $oExtension->sLabel;
if (!$oExtension->CanBeUninstalled()) {
$aExtensionsNotUninstallable[$oExtension->sCode] = true;
}
} elseif (!$oExtension->bInstalled && $bSelected) {
$aExtensionsAdded[$oExtension->sCode] = $oExtension->sLabel;
}
}
$sExtensionsAdded = '';
if (count($aExtensionsAdded) > 0) {
$sExtensionsAdded = '<ul>';
foreach ($aExtensionsAdded as $sExtensionCode) {
$sExtensionsAdded .= '<li>'.$sExtensionCode.'</li>';
}
$sExtensionsAdded .= '</ul>';
} else {
$sExtensionsAdded = '<ul><li>No extension added.</li></ul>';
}
$sExtensionsRemoved = '';
if (count($aExtensionsRemoved) > 0) {
$sExtensionsRemoved = '<ul>';
foreach ($aExtensionsRemoved as $sCode => $sExtensionCode) {
$sForcedUninstall = '';
if (isset($aExtensionsNotUninstallable[$sCode])) {
$sForcedUninstall = ' (forced uninstallation)';
}
$sExtensionsRemoved .= '<li>'.$sExtensionCode.$sForcedUninstall.'</li>';
}
$sExtensionsRemoved .= '</ul>';
} else {
$sExtensionsRemoved = '<ul><li>No extension removed.</li></ul>';
}
return [$sExtensionsAdded, $sExtensionsRemoved];
}
public function ProcessParams($bMoveForward = true)
{
// Accumulates the selected modules:
@@ -1396,9 +1443,14 @@ class WizStepModulesChoice extends WizardStep
if (class_exists('CreateITILProfilesInstaller')) {
$this->oWizard->SetParameter('old_addon', true);
}
[$sExtensionsAdded, $sExtensionsRemoved] = $this->GetAddedAndRemovedExtensions($aExtensions);
$this->oWizard->SetParameter('selected_modules', json_encode(array_keys($aModules)));
$this->oWizard->SetParameter('selected_extensions', json_encode($aExtensions));
$this->oWizard->SetParameter('display_choices', $sDisplayChoices);
$this->oWizard->SetParameter('extensions_added', $sExtensionsAdded);
$this->oWizard->SetParameter('extensions_removed', $sExtensionsRemoved);
return ['class' => 'WizStepSummary', 'state' => ''];
}
@@ -1561,7 +1613,7 @@ EOF
}
}
$aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : [];
$aAlternatives = $aInfo['alternatives'] ?? [];
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
@@ -1605,7 +1657,7 @@ EOF
if ($this->bUpgrade) {
// In upgrade mode, the defaults are the installed modules
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed
$aScores[$sChoiceId][$sModuleId] = true;
}
@@ -1663,7 +1715,7 @@ EOF
}
if (array_key_exists('modules', $aChoice)) {
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed, increase the score of this choice
if (!isset($aScores[$sChoiceId])) {
$aScores[$sChoiceId] = [];
@@ -1952,10 +2004,47 @@ EOF
return '<i class="setup-extension--icon '.$sDecorationClass.'" data-tooltip-content="'.$sResult.'"></i>';
}
public function ComputeChoiceFlags(array $aChoice, string $sChoiceId, array $aSelectedComponents, bool $bAllDisabled, bool $bDisableUninstallCheck, bool $bUpgradeMode)
{
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $bUpgradeMode && $oITopExtension->bInstalled && !$bCanBeUninstalled && !$bDisableUninstallCheck;
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bDisabled = $bMandatory || $bAllDisabled || $bMissingFromDisk;
$bChecked = $bMandatory || $bSelected;
if (isset($aChoice['sub_options'])) {
$aOptions = $aChoice['sub_options']['options'] ?? [];
foreach ($aOptions as $index => $aSubChoice) {
$sSubChoiceId = $sChoiceId.self::$SEP.$index;
$aSubFlags = $this->ComputeChoiceFlags($aSubChoice, $sSubChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $bUpgradeMode);
if ($aSubFlags['checked']) {
$bChecked = true;
if ($aSubFlags['disabled']) {
//If some sub options are enabled and cannot be disabled, this choice should also cannot be disabled since it would disable all its sub options
$bDisabled = true;
}
}
}
}
return [
'uninstallable' => $bCanBeUninstalled,
'missing' => $bMissingFromDisk,
'installed' => $bInstalled,
'disabled' => $bDisabled,
'checked' => $bChecked,
];
}
protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false)
{
$aOptions = isset($aStepInfo['options']) ? $aStepInfo['options'] : [];
$aAlternatives = isset($aStepInfo['alternatives']) ? $aStepInfo['alternatives'] : [];
$aOptions = $aStepInfo['options'] ?? [];
$aAlternatives = $aStepInfo['alternatives'] ?? [];
$bDisableUninstallCheck = (bool)$this->oWizard->GetParameter('force-uninstall', false);
@@ -1963,43 +2052,33 @@ EOF
$sChoiceId = $sParentId.self::$SEP.$index;
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
$sId = utils::EscapeHtml($aChoice['extension_code']);
$bIsDefault = array_key_exists($sChoiceId, $aDefaults);
$oITopExtension = $this->oExtensionsMap->GetFromExtensionCode($aChoice['extension_code']);
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $this->bUpgrade && $bIsDefault && !$bCanBeUninstalled && !$bDisableUninstallCheck;
;
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bDisabled = $bMandatory || $bAllDisabled || $bMissingFromDisk;
$bChecked = $bMandatory || $bSelected;
$aFlags = static::ComputeChoiceFlags($aChoice, $sChoiceId, $aSelectedComponents, $bAllDisabled, $bDisableUninstallCheck, $this->bUpgrade);
$sTooltip = '';
$sUnremovable = '';
if ($bMissingFromDisk) {
if ($aFlags['missing']) {
$sTooltip .= '<span class="setup-extension-tag removed">source removed</span>';
}
if ($bInstalled) {
if ($aFlags['installed']) {
$sTooltip .= '<span class="setup-extension-tag checked installed">installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</span>';
} else {
$sTooltip .= '<span class="setup-extension-tag checked tobeinstalled">to be installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked notinstalled">not installed</span>';
}
if (!$bCanBeUninstalled) {
if (!$aFlags['uninstallable']) {
$sTooltip .= '<span class="setup-extension-tag notuninstallable">cannot be uninstalled</span>';
}
if ($bDisabled && !$bChecked && !$bCanBeUninstalled && !$bDisableUninstallCheck) {
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
}
$sChecked = $bChecked ? ' checked ' : '';
$sDisabled = $bDisabled ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $bMissingFromDisk ? 'setup-extension--missing' : '';
$sChecked = $aFlags['checked'] ? ' checked ' : '';
$sDisabled = $aFlags['disabled'] ? ' disabled data-disabled="disabled" ' : '';
$sMissingModule = $aFlags['missing'] ? 'setup-extension--missing' : '';
$sHiddenInput = $bDisabled && $bChecked ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$sHiddenInput = $aFlags['disabled'] && $aFlags['checked'] ? '<input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'"/>' : '';
$oPage->add('<div class="choice '.$sMissingModule.'" '.$sDataId.'><input class="wiz-choice '.$sUnremovable.'" id="'.$sId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'" '.$sDisabled.$sChecked.'/>'.$sHiddenInput.'&nbsp;');
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled, $sTooltip);
$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $aFlags['disabled'], $sTooltip);
$oPage->add('</div>');
}
$sChoiceName = null;
@@ -2034,7 +2113,6 @@ EOF
$sChoiceId = $sParentId.self::$SEP.$index;
$sDataId = 'data-id="'.utils::EscapeHtml($aChoice['extension_code']).'"';
$sId = utils::EscapeHtml($aChoice['extension_code']);
if ($sChoiceName == null) {
$sChoiceName = $sChoiceId; // All radios share the same name
}
@@ -2114,6 +2192,18 @@ class WizStepSummary extends WizardStep
$this->bDependencyCheck = true;
try {
SetupUtils::AnalyzeInstallation($this->oWizard, true, $aSelectedModules);
/*$sInstallMode = utils::ReadParam('install_mode');
\SetupLog::Info(__METHOD__, null, ['$sInstallMode' => $sInstallMode]);
//if ($sInstallMode === "upgrade") {
$aExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true);
$oSetupAudit = new SetupAudit([]);
$oConfig = SetupUtils::GetConfig($this->oWizard);
$oSetupAudit->SetSelectedExtensions($oConfig, $aExtensions);
//$oSetupAudit->AuditExtensionsCleanupRules(true);
//}
*/
} catch (MissingDependencyException $e) {
$this->bDependencyCheck = false;
$this->sDependencyIssue = $e->getHtmlDesc();
@@ -2178,6 +2268,14 @@ class WizStepSummary extends WizardStep
$oPage->add('<fieldset id="summary"><legend>Installation Parameters</legend>');
$oPage->add('<div id="params_summary">');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be installed</span>');
$oPage->add($this->oWizard->GetParameter('extensions_added'));
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be uninstalled</span>');
$oPage->add($this->oWizard->GetParameter('extensions_removed'));
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Database Parameters</span><ul>');
$oPage->add('<li>Server Name: '.$aInstallParams['database']['server'].'</li>');
$oPage->add('<li>DB User Name: '.$aInstallParams['database']['user'].'</li>');

View File

@@ -1019,10 +1019,10 @@ EOF
if ($sModuleId == '_Root_') {
continue;
}
if ($aModuleData['version_db'] == '') {
if ($aModuleData['installed_version'] == '') {
continue;
}
$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['version_db']."\n");
$oPage->add('InstalledModule/'.$sModuleId.': '.$aModuleData['installed_version']."\n");
}
$oPage->add('===== end =====');

View File

@@ -54,10 +54,20 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
*/
abstract public function GetDatamodelDeltaAbsPath(): string;
/**
* @return array<string, string> : dict extensions folders by their code
*/
public function GetAdditionalFeaturePaths(): array
{
return [];
}
protected function setUp(): void
{
static::LoadRequiredItopFiles();
$this->oEnvironment = new UnitTestRunTimeEnvironment('production', $this->GetTestEnvironment());
if (is_null($this->oEnvironment)) {
$this->oEnvironment = new UnitTestRunTimeEnvironment($this->GetTestEnvironment());
}
parent::setUp();
}
@@ -123,7 +133,6 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
// Note: To improve performances, we compile all XML deltas from test cases derived from this class and make a single environment where everything will be ran at once.
// This requires XML deltas to be compatible, but it is a known and accepted trade-off. See PR #457
if (false === $this->IsEnvironmentReady()) {
$this->debug("Preparing custom environment '$sTestEnv' with the following datamodel files:");
foreach ($this->oEnvironment->GetCustomDatamodelFiles() as $sCustomDatamodelFile) {
$this->debug(" - $sCustomDatamodelFile");
@@ -155,24 +164,33 @@ abstract class ItopCustomDatamodelTestCase extends ItopDataTestCase
$oTestConfig->ChangeModulesPath($sSourceEnv, $sTestEnv);
// - Switch DB name to a dedicated one so we don't mess with the original one
$sTestEnvSanitizedForDBName = preg_replace('/[^\d\w]/', '', $sTestEnv);
$oTestConfig->Set('db_name', $oTestConfig->Get('db_name').'_'.$sTestEnvSanitizedForDBName);
$sPreviousDB = $oTestConfig->Get('db_name');
$sNewDB = $sPreviousDB.'_'.$sTestEnvSanitizedForDBName;
$oTestConfig->Set('db_name', $sNewDB);
// - Compile env. based on the existing 'production' env.
$oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv);
$oEnvironment->WriteConfigFileSafe($oTestConfig);
$oEnvironment->CompileFrom($sSourceEnv);
//$oEnvironment = new UnitTestRunTimeEnvironment($sSourceEnv, $sTestEnv);
$this->oEnvironment->WriteConfigFileSafe($oTestConfig);
$this->oEnvironment->CompileFrom($sSourceEnv);
// - Force re-creating a fresh DB
CMDBSource::InitFromConfig($oTestConfig);
if (CMDBSource::IsDB($oTestConfig->Get('db_name'))) {
if (CMDBSource::IsDB($sNewDB)) {
CMDBSource::DropDB();
}
CMDBSource::CreateDB($oTestConfig->Get('db_name'));
CMDBSource::CreateDB($sNewDB);
MetaModel::Startup($sConfFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sTestEnv);
// N°7446 For some reason we need to create the DB schema before starting the MM, then only we can create the tables.
MetaModel::DBCreate();
// Make sure that runtime environment is complete
// RunTimeEnvironment::AnalyzeInstallation would not return core modules otherwise...
CMDBSource::DropTable("priv_module_install");
CMDBSource::Query("CREATE TABLE $sNewDB.priv_module_install SELECT * FROM $sPreviousDB.priv_module_install");
$this->debug("Custom environment '$sTestEnv' is ready!");
} else {
$this->debug("Custom environment '$sTestEnv' READY BUILT:");
}
parent::PrepareEnvironment();

View File

@@ -26,26 +26,25 @@ use utils;
*/
class UnitTestRunTimeEnvironment extends RunTimeEnvironment
{
/**
* @var false
*/
public bool $bUseDelta = true;
/**
* @var true
*/
public bool $bUseAdditionalFeatures = false;
/**
* @var string[]
*/
protected $aCustomDatamodelFiles = null;
/**
* @var string
* @var string[]
*/
protected $sSourceEnv;
public function __construct($sSourceEnv, $sTargetEnv)
{
parent::__construct($sTargetEnv);
$this->sSourceEnv = $sSourceEnv;
}
public function GetEnvironment(): string
{
return $this->sFinalEnv;
}
protected $aAdditionExtensionFoldersByCode = null;
public function CompileFrom($sSourceEnv, $bUseSymLinks = null)
{
@@ -56,6 +55,15 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
SetupUtils::copydir(APPROOT.'/data/'.$sSourceEnv.'-modules', $sDestModulesDir, $bUseSymLinks);
if ($this->bUseAdditionalFeatures) {
foreach ($this->GetExtensionFoldersToAdd() as $sExtensionCode => $sFolderPath) {
\SetupLog::Info("ExtensionFoldersToAdd: $sExtensionCode => $sFolderPath");
$sFolderName = basename($sFolderPath);
@mkdir($sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName);
SetupUtils::copydir($sFolderPath, $sDestModulesDir.DIRECTORY_SEPARATOR.$sFolderName, $bUseSymLinks);
}
}
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
}
@@ -94,23 +102,43 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
*/
protected function GetMFModulesToCompile($sSourceEnv, $sSourceDir)
{
\SetupLog::Info(__METHOD__);
$aRet = parent::GetMFModulesToCompile($sSourceEnv, $sSourceDir);
foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) {
$sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile);
$sDeltaName = basename($sDeltaFile);
$sDeltaDir = dirname($sDeltaFile);
$oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile);
$aRet[$sDeltaId] = $oDelta;
if ($this->bUseDelta) {
foreach ($this->GetCustomDatamodelFiles() as $sDeltaFile) {
$sDeltaId = preg_replace('/[^\d\w]/', '', $sDeltaFile);
$sDeltaName = basename($sDeltaFile);
$sDeltaDir = dirname($sDeltaFile);
$oDelta = new MFCoreModule($sDeltaName, "$sDeltaDir/$sDeltaName", $sDeltaFile);
$aRet[$sDeltaId] = $oDelta;
}
}
return $aRet;
}
public function GetCustomDatamodelFiles()
public function GetExtensionFoldersToAdd(): array
{
if (!is_null($this->aCustomDatamodelFiles)) {
return $this->aCustomDatamodelFiles;
if (is_null($this->aAdditionExtensionFoldersByCode)) {
$this->InitViaItopCustomDatamodelTestCaseClasses();
}
return $this->aAdditionExtensionFoldersByCode;
}
public function GetCustomDatamodelFiles(): array
{
if (is_null($this->aCustomDatamodelFiles)) {
$this->InitViaItopCustomDatamodelTestCaseClasses();
}
return $this->aCustomDatamodelFiles;
}
public function InitViaItopCustomDatamodelTestCaseClasses()
{
$this->aAdditionExtensionFoldersByCode = [];
$this->aCustomDatamodelFiles = [];
// Search for the PHP files implementing the method GetDatamodelDeltaAbsPath
@@ -169,16 +197,19 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
continue;
}
$sDeltaFile = $oTestClassInstance->GetDatamodelDeltaAbsPath();
if (!is_file($sDeltaFile)) {
throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'");
}
if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) {
$this->aCustomDatamodelFiles[] = $sDeltaFile;
if (strlen($sDeltaFile) > 0) {
if (!is_file($sDeltaFile)) {
throw new \Exception("Unknown delta file: $sDeltaFile, from test class '$sClass'");
}
if (!in_array($sDeltaFile, $this->aCustomDatamodelFiles)) {
$this->aCustomDatamodelFiles[] = $sDeltaFile;
}
}
$aExtensionsPaths = $oTestClassInstance->GetAdditionalFeaturePaths();
$this->aAdditionExtensionFoldersByCode = array_merge($this->aAdditionExtensionFoldersByCode, $aExtensionsPaths);
}
}
return $this->aCustomDatamodelFiles;
}
private function FindFilesModifiedAfter(float $fReferenceTimestamp, string $sPathToScan, array &$aModifiedFiles)

View File

@@ -7,30 +7,26 @@
*
*
*/
$MySettings = array(
$MySettings = [
// app_root_url: Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server's name)
// default: ''
'app_root_url' => 'http://%server(SERVER_NAME)?:localhost%/itop/iTop/',
);
];
/**
*
* Modules specific settings
*
*/
$MyModuleSettings = array(
);
$MyModuleSettings = [
];
/**
*
* Data model modules to be loaded. Names are specified as relative paths
*
*/
$MyModules = array(
);
?>
$MyModules = [
];

View File

@@ -7,30 +7,26 @@
*
*
*/
$MySettings = array(
$MySettings = [
// app_root_url: Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server's name)
// default: ''
'app_root_url' => 'http://' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost') . '/itop/iTop/',
'app_root_url' => 'http://'.(isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost').'/itop/iTop/',
);
];
/**
*
* Modules specific settings
*
*/
$MyModuleSettings = array(
);
$MyModuleSettings = [
];
/**
*
* Data model modules to be loaded. Names are specified as relative paths
*
*/
$MyModules = array(
);
?>
$MyModules = [
];

View File

@@ -0,0 +1,164 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use AnalyzeInstallation;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ModuleInstallationService;
class AnalyzeInstallationTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/AnalyzeInstallation.php');
$this->RequireOnceItopFile('setup/ModuleInstallationService.php');
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
public static function AnalyzeInstallationProvider()
{
//$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules.json'), true);
$aAnalyzeInstallationOutput = json_decode(file_get_contents(__DIR__.'/ressources/analyze_installation_output.json'), true);
$aAvailableModules = json_decode(file_get_contents(__DIR__.'/ressources/available_modules.json'), true);
return [
'new modules not in DB setup history' => [
'aAvailableModules' => [
'mandatory_module/1.0.0' => [
"mandatory" => true,
"ga" => "bu",
],
'optional_module/6.6.6' => [
"mandatory" => false,
"zo" => "meu",
],
],
'aInstalledModules' => [],
'expected' => [
'_Root_' => [
'installed_version' => '',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
'mandatory_module' => [
"mandatory" => true,
'installed_version' => '',
'available_version' => '1.0.0',
'install' => [
'flag' => 2,
'message' => 'the module is part of the application',
],
"ga" => "bu",
],
'optional_module' => [
"mandatory" => false,
'installed_version' => '',
'available_version' => '6.6.6',
"zo" => "meu",
'install' => [
'flag' => 1,
'message' => '',
],
],
],
],
'new modules ALREADY in DB setup history' => [
'aAvailableModules' => [
'mandatory_module/1.0.0' => [
"mandatory" => true,
"ga" => "bu",
],
'optional_module/6.6.6' => [
"mandatory" => false,
"zo" => "meu",
],
],
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules_simpleusecase.json'), true),
'expected' => [
'_Root_' => [
'installed_version' => '3.3.0-dev-svn',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
'mandatory_module' => [
"mandatory" => true,
'installed_version' => '3.3.0',
'available_version' => '1.0.0',
"ga" => "bu",
'install' => [
'flag' => 2,
'message' => 'the module is part of the application',
],
'uninstall' => [
'flag' => 3,
'message' => 'the module is part of the application',
],
],
'optional_module' => [
"mandatory" => false,
'installed_version' => '3.3.0',
'available_version' => '6.6.6',
"zo" => "meu",
'install' => [
'flag' => 1,
'message' => '',
],
'uninstall' => [
'flag' => 1,
'message' => '',
],
],
],
],
'dummyfirst installation' => [
'aAvailableModules' => [],
'aInstalledModules' => [],
'expected' => [
'_Root_' => [
'installed_version' => '',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
],
],
'dummy 2nd installation' => [
'aAvailableModules' => [],
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true),
'expected' => [
'_Root_' => [
'installed_version' => '3.3.0-dev-svn',
'available_version' => 'ITOP_VERSION_FULL',
'name_code' => 'ITOP_APPLICATION',
],
],
],
'real_case' => [
'aAvailableModules' => $aAvailableModules,
'aInstalledModules' => json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true),
'expected' => $aAnalyzeInstallationOutput,
],
];
}
/**
* @dataProvider AnalyzeInstallationProvider
*/
public function testAnalyzeInstallation($aAvailableModules, $aInstalledModules, $expected)
{
$sContent = str_replace(['ITOP_VERSION_FULL', 'ITOP_APPLICATION'], [ITOP_VERSION_FULL, ITOP_APPLICATION], json_encode($expected));
$expected = json_decode($sContent, true);
$this->SetNonPublicProperty(AnalyzeInstallation::GetInstance(), "aAvailableModules", $aAvailableModules);
//$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true);
$this->SetNonPublicProperty(ModuleInstallationService::GetInstance(), "aSelectInstall", $aInstalledModules);
$oConfig = $this->createMock(\Config::class);
$modulesPath = [
APPROOT.'extensions',
];
$aModules = AnalyzeInstallation::GetInstance()->AnalyzeInstallation($oConfig, $modulesPath, false, null);
$this->assertEquals($expected, $aModules);
}
}

View File

@@ -0,0 +1,14 @@
<?php
class WizStepModulesChoiceFake extends WizStepModulesChoice
{
public function __construct(WizardController $oWizard, $sCurrentState)
{
}
public function setExtensionMap(iTopExtensionsMap $oMap)
{
$this->oExtensionsMap = $oMap;
}
}

View File

@@ -0,0 +1,355 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ItopExtensionsMap;
use iTopExtensionsMapFake;
use ModuleDiscovery;
use WizardController;
class WizStepModulesChoiceTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('/setup/unattended-install/InstallationFileService.php');
require_once __DIR__.'/iTopExtensionsMapFake.php';
require_once __DIR__.'/WizStepModulesChoiceFake.php';
$this->oStep = new \WizStepModulesChoiceFake(new WizardController('', ''), '');
ModuleDiscovery::ResetCache();
}
public function ProviderComputeChoiceFlags()
{
return [
'selected but not installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => false,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => ['_0' => '_0'],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => true,
],
],
'not selected, not installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'installed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => false,
'checked' => false,
],
],
'installed non uninstallable extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => false,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
'mandatory extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'uninstallable' => true,
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'optional sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => true,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => false,
'checked' => false,
],
],
'mandatory sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
'itop-ext1-1' => [
'installed' => false,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => true,
'uninstallable' => true,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => false,
'disabled' => true,
'checked' => true,
],
],
'non uninstallable sub extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
'itop-ext1-1' => [
'installed' => true,
],
],
'bUpgrade' => true,
'bDisableUninstallCheck' => false,
'sChoiceId' => '_0',
'aStepInfo' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'uninstallable' => true,
'sub_options' => [
'options' => [
[
'extension_code' => 'itop-ext1-1',
'mandatory' => false,
'uninstallable' => false,
],
],
],
],
'aSelected' => [],
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => false,
'installed' => true,
'disabled' => true,
'checked' => true,
],
],
];
}
/**
* @dataProvider ProviderComputeChoiceFlags
*/
public function testComputeChoiceFlags($aExtensions, $bUpgrade, $bDisableUninstallCheck, $sChoiceId, $aStepInfo, $aSelected, $aExpectedFlags)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensions));
$aFlags = $this->oStep->ComputeChoiceFlags($aStepInfo, $sChoiceId, $aSelected, false, $bDisableUninstallCheck, $bUpgrade);
$this->assertEquals($aExpectedFlags, $aFlags);
}
public function ProviderGetAddedAndRemovedExtensions()
{
return [
'no extensions' => [
'aExtensions' => [],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'no extensions selected' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'no extensions removed' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'aSelected' => ['itop-ext1'],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'One added extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => false,
],
],
'aSelected' => ['itop-ext1'],
'sExpectedAddedList' => '<ul><li>itop-ext1</li></ul>',
'sExpectedRemovedList' => '<ul><li>No extension removed.</li></ul>',
],
'One removed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext1</li></ul>',
],
'Forced removed extension' => [
'aExtensions' => [
'itop-ext1' => [
'installed' => true,
'uninstallable' => false,
],
],
'aSelected' => [],
'sExpectedAddedList' => '<ul><li>No extension added.</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext1 (forced uninstallation)</li></ul>',
],
'added and removed extensions' => [
'aExtensions' => [
'itop-ext-added1' => [
'installed' => false,
],
'itop-ext-added2' => [
'installed' => false,
],
'itop-ext-removed1' => [
'installed' => true,
],
'itop-ext-removed2' => [
'installed' => true,
],
],
'aSelected' => ['itop-ext-added1', 'itop-ext-added2'],
'sExpectedAddedList' => '<ul><li>itop-ext-added1</li><li>itop-ext-added2</li></ul>',
'sExpectedRemovedList' => '<ul><li>itop-ext-removed1</li><li>itop-ext-removed2</li></ul>',
],
];
}
/**
* @dataProvider ProviderGetAddedAndRemovedExtensions
*/
public function testGetAddedAndRemovedExtensions($aExtensions, $aSelectedExtensions, $sExpectedAddedList, $sExpectedRemovedList)
{
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensions));
[$sAddedList, $sRemovedList] = $this->oStep->GetAddedAndRemovedExtensions($aSelectedExtensions);
$this->assertEquals($sExpectedAddedList, $sAddedList);
$this->assertEquals($sExpectedRemovedList, $sRemovedList);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
class ModelSerializationTest extends ItopDataTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php');
}
public function testGetModelFromEnvironment()
{
$aModel = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->GetTestEnvironment());
$this->assertEqualsCanonicalizing(MetaModel::GetClasses(), $aModel);
}
public function testGetModelFromEnvironmentFailure()
{
$this->expectException(\CoreException::class);
$this->expectExceptionMessage("Cannot get classes");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use Exception;
class SetupAuditTest extends ItopCustomDatamodelTestCase
{
public const ENVT = 'php-unit-extensionremoval-tests';
public function GetDatamodelDeltaAbsPath(): string
{
//no delta: empty path provided
return "";
}
public function GetAdditionalFeaturePaths(): array
{
$aFeaturePaths = [];
foreach (glob(__DIR__."/additional_features/*", GLOB_ONLYDIR) as $aFeaturePath) {
$sCode = basename($aFeaturePath);
$aFeaturePaths[$sCode] = $aFeaturePath;
}
return $aFeaturePaths;
}
protected function setUp(): void
{
static::LoadRequiredItopFiles();
$this->oEnvironment = new UnitTestRunTimeEnvironment(self::ENVT);
$this->oEnvironment->bUseDelta = false;
$this->oEnvironment->bUseAdditionalFeatures = true;
parent::setUp();
$this->RequireOnceItopFile('/setup/feature_removal/SetupAudit.php');
$this->RequireOnceItopFile('/setup/feature_removal/DryRemovalRuntimeEnvironment.php');
}
public function GetTestEnvironment(): string
{
return self::ENVT;
}
public function testComputeDryRemoval()
{
$oDryRemovalRuntimeEnvt = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvt->Prepare($this->GetTestEnvironment(), ['nominal_ext1', 'finalclass_ext2']);
$oDryRemovalRuntimeEnvt->CompileFrom($this->GetTestEnvironment());
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$expected = [
"Feature1Module1MyClass",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());
$expected = [
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
public function testGetIssues()
{
$sUID = "AuditExtensionsCleanupRules_".uniqid();
$oOrg = $this->CreateOrganization($sUID);
$this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
"FinalClassFeature1Module1MyFinalClassFromLocation",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$expected = [
"FinalClassFeature1Module1MyFinalClassFromLocation" => 1,
"FinalClassFeature2Module1MyFinalClassFromLocation" => 0,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
public function testAuditExtensionsCleanupRulesFailASAP()
{
$sUID = "AuditExtensionsCleanupRules_".uniqid();
$oOrg = $this->CreateOrganization($sUID);
$this->createObject('FinalClassFeature1Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$this->createObject('FinalClassFeature2Module1MyFinalClassFromLocation', ['org_id' => $oOrg->GetKey(), 'name' => $sUID, 'name2' => uniqid()]);
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
"FinalClassFeature1Module1MyFinalClassFromLocation",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
];
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$this->expectException(Exception::class);
$this->expectExceptionMessage('FinalClassFeature1Module1MyFinalClassFromLocation');
$oSetupAudit->GetIssues(true);
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>finalclass_ext1</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>finalclass_ext1_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="FinalClassFeature1Module1MyFinalClassFromLocation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature1Module1MyFinalClassFromLocation</db_table>
<naming>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name2" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name2" xsi:type="AttributeString">
<sql>name2</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<methods/>
<presentation/>
<parent>Location</parent>
</class>
<class id="FinalClassFeature1Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature1Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

View File

@@ -0,0 +1,16 @@
<?php
//// PHP Data Model definition file
//
//// WARNING - WARNING - WARNING
//// DO NOT EDIT THIS FILE (unless you know what you are doing)
////
//// If you use supply a datamodel.xxxx.xml file with your module
//// the this file WILL BE overwritten by the compilation of the
//// module (during the setup) if the datamodel.xxxx.xml file
//// contains the definition of new classes or menus.
////
//// The recommended way to define new classes (for iTop 2.0) is via the XML definition.
//// This file remains in the module's template only for the cases where there is:
//// - either no new class or menu defined in the XML file
//// - or no XML file at all supplied by the module

View File

@@ -0,0 +1,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'finalclass_ext1_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.finalclass_ext1_module1.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [// Module specific settings go here, if any
],
]
);

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>finalclass_ext2</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>finalclass_ext2_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="FinalClassFeature2Module1MyFinalClassFromLocation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature2Module1MyFinalClassFromLocation</db_table>
<naming>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name2"/>
<attribute id="finalclass"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name2" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name2" xsi:type="AttributeString">
<sql>name2</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<methods/>
<presentation/>
<parent>Location</parent>
</class>
<class id="FinalClassFeature2Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>FinalClassFeature2Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

View File

@@ -0,0 +1,16 @@
<?php
//// PHP Data Model definition file
//
//// WARNING - WARNING - WARNING
//// DO NOT EDIT THIS FILE (unless you know what you are doing)
////
//// If you use supply a datamodel.xxxx.xml file with your module
//// the this file WILL BE overwritten by the compilation of the
//// module (during the setup) if the datamodel.xxxx.xml file
//// contains the definition of new classes or menus.
////
//// The recommended way to define new classes (for iTop 2.0) is via the XML definition.
//// This file remains in the module's template only for the cases where there is:
//// - either no new class or menu defined in the XML file
//// - or no XML file at all supplied by the module

View File

@@ -0,0 +1,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'finalclass_ext2_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.finalclass_ext2_module1.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [// Module specific settings go here, if any
],
]
);

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension format="1.0">
<extension_code>nominal_ext1</extension_code>
<company>Combodo SARL</company>
<author><![CDATA[Odain]]></author>
<label><![CDATA[Ext For Test]]></label>
<description><![CDATA[Ext For Test]]></description>
<version>6.6.6</version>
<modules type="array">
<module>
<id>nominal_ext1_module1</id>
<version>tags/6.6.6</version>
</module>
</modules>
<release_date>2023-07-19</release_date>
<version_description><![CDATA[
]]></version_description>
<itop_version_min>3.2.0</itop_version_min>
<status></status>
<mandatory>false</mandatory>
<more_info_url></more_info_url>
</extension>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<classes>
<class id="Feature1Module1MyClass" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>Feature1Module1MyClass</db_table>
<naming>
<attributes>
<attribute id="name"/>
</attributes>
</naming>
<reconciliation>
<attributes>
<attribute id="name"/>
</attributes>
</reconciliation>
<order>
<columns>
<column id="name" ascending="false"/>
</columns>
</order>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
</fields>
<methods/>
<presentation/>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<menus/>
<user_rights/>
<module_parameters/>
</itop_design>

View File

@@ -0,0 +1,16 @@
<?php
//// PHP Data Model definition file
//
//// WARNING - WARNING - WARNING
//// DO NOT EDIT THIS FILE (unless you know what you are doing)
////
//// If you use supply a datamodel.xxxx.xml file with your module
//// the this file WILL BE overwritten by the compilation of the
//// module (during the setup) if the datamodel.xxxx.xml file
//// contains the definition of new classes or menus.
////
//// The recommended way to define new classes (for iTop 2.0) is via the XML definition.
//// This file remains in the module's template only for the cases where there is:
//// - either no new class or menu defined in the XML file
//// - or no XML file at all supplied by the module

View File

@@ -0,0 +1,51 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'nominal_ext1_module1/6.6.6',
[
// Identification
//
'label' => 'Ext For Test',
'category' => 'business',
// Setup
//
'dependencies' => [
'itop-structure/3.2.0',
],
'mandatory' => false,
'visible' => true,
'installer' => '',
// Components
//
'datamodel' => [
'model.nominal_ext1_module1.php',
],
'webservice' => [],
'data.struct' => [// add your 'structure' definition XML files here,
],
'data.sample' => [// add your sample data XML files here,
],
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => [// Module specific settings go here, if any
],
]
);

View File

@@ -0,0 +1,29 @@
<?php
class iTopExtensionsMapFake extends iTopExtensionsMap
{
public function __construct($sFromEnvironment = 'production', $aExtraDirs = [])
{
$this->aExtensions = [];
$this->aExtensionsByCode = [];
$this->aScannedDirs = [];
}
public static function createFromArray($aExtensions)
{
$oMap = new static();
foreach ($aExtensions as $sCode => $aExtension) {
$oExtension = new iTopExtension();
$oExtension->sCode = $sCode;
$oExtension->sLabel = $sCode;
$oExtension->bInstalled = $aExtension['installed'];
$oExtension->aModules = $aExtension['modules'] ?? [];
$oExtension->bCanBeUninstalled = $aExtension['uninstallable'] ?? null;
$oExtension->sVersion = $aExtension['version'] ?? '1.0.0';
$oExtension->aModuleInfo = [];
$oMap->AddExtension($oExtension);
}
return $oMap;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,642 @@
[
{
"0": "330",
"id": "330",
"1": "iTop",
"name": "iTop",
"2": "3.3.0-dev-svn",
"version": "3.3.0-dev-svn",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nBuilt on $WCNOW$",
"comment": "Done by the setup program\nBuilt on $WCNOW$",
"5": "0",
"parent_id": "0",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "331",
"id": "331",
"1": "authent-cas",
"name": "authent-cas",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "332",
"id": "332",
"1": "authent-external",
"name": "authent-external",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "333",
"id": "333",
"1": "authent-ldap",
"name": "authent-ldap",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "334",
"id": "334",
"1": "authent-local",
"name": "authent-local",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "335",
"id": "335",
"1": "combodo-backoffice-darkmoon-theme",
"name": "combodo-backoffice-darkmoon-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "336",
"id": "336",
"1": "combodo-backoffice-fullmoon-high-contrast-theme",
"name": "combodo-backoffice-fullmoon-high-contrast-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "337",
"id": "337",
"1": "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme",
"name": "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "338",
"id": "338",
"1": "combodo-backoffice-fullmoon-tritanopia-theme",
"name": "combodo-backoffice-fullmoon-tritanopia-theme",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "339",
"id": "339",
"1": "itop-backup",
"name": "itop-backup",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "340",
"id": "340",
"1": "itop-config",
"name": "itop-config",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "341",
"id": "341",
"1": "itop-files-information",
"name": "itop-files-information",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "342",
"id": "342",
"1": "itop-portal-base",
"name": "itop-portal-base",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "343",
"id": "343",
"1": "itop-profiles-itil",
"name": "itop-profiles-itil",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "344",
"id": "344",
"1": "itop-sla-computation",
"name": "itop-sla-computation",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "345",
"id": "345",
"1": "itop-structure",
"name": "itop-structure",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "346",
"id": "346",
"1": "itop-welcome-itil",
"name": "itop-welcome-itil",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"comment": "Done by the setup program\nMandatory\nHidden (selected automatically)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "347",
"id": "347",
"1": "itop-config-mgmt",
"name": "itop-config-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "348",
"id": "348",
"1": "itop-attachments",
"name": "itop-attachments",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "349",
"id": "349",
"1": "itop-tickets",
"name": "itop-tickets",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "350",
"id": "350",
"1": "combodo-db-tools",
"name": "combodo-db-tools",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "351",
"id": "351",
"1": "itop-core-update",
"name": "itop-core-update",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-files-information\/2.7.0\nDepends on module: combodo-db-tools\/2.7.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-files-information\/2.7.0\nDepends on module: combodo-db-tools\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "352",
"id": "352",
"1": "itop-hub-connector",
"name": "itop-hub-connector",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "353",
"id": "353",
"1": "itop-oauth-client",
"name": "itop-oauth-client",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-welcome-itil\/3.1.0,",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-welcome-itil\/3.1.0,",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "354",
"id": "354",
"1": "itop-themes-compat",
"name": "itop-themes-compat",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.1.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-structure\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "355",
"id": "355",
"1": "itop-datacenter-mgmt",
"name": "itop-datacenter-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "356",
"id": "356",
"1": "itop-endusers-devices",
"name": "itop-endusers-devices",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "357",
"id": "357",
"1": "itop-storage-mgmt",
"name": "itop-storage-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "358",
"id": "358",
"1": "itop-virtualization-mgmt",
"name": "itop-virtualization-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "359",
"id": "359",
"1": "itop-bridge-cmdb-ticket",
"name": "itop-bridge-cmdb-ticket",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-tickets\/2.7.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-tickets\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "360",
"id": "360",
"1": "itop-bridge-virtualization-storage",
"name": "itop-bridge-virtualization-storage",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-storage-mgmt\/2.2.0\nDepends on module: itop-virtualization-mgmt\/2.2.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-storage-mgmt\/2.2.0\nDepends on module: itop-virtualization-mgmt\/2.2.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "361",
"id": "361",
"1": "itop-service-mgmt",
"name": "itop-service-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "362",
"id": "362",
"1": "itop-bridge-cmdb-services",
"name": "itop-bridge-cmdb-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "363",
"id": "363",
"1": "itop-bridge-datacenter-mgmt-services",
"name": "itop-bridge-datacenter-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-datacenter-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-datacenter-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "364",
"id": "364",
"1": "itop-bridge-endusers-devices-services",
"name": "itop-bridge-endusers-devices-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-endusers-devices\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-endusers-devices\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "365",
"id": "365",
"1": "itop-bridge-storage-mgmt-services",
"name": "itop-bridge-storage-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-storage-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-storage-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "366",
"id": "366",
"1": "itop-bridge-virtualization-mgmt-services",
"name": "itop-bridge-virtualization-mgmt-services",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-virtualization-mgmt\/3.1.0",
"comment": "Done by the setup program\nOptional\nHidden (selected automatically)\nDepends on module: itop-config-mgmt\/2.7.1\nDepends on module: itop-service-mgmt\/2.7.1 || itop-service-mgmt-provider\/2.7.1\nDepends on module: itop-virtualization-mgmt\/3.1.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "367",
"id": "367",
"1": "itop-request-mgmt",
"name": "itop-request-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.4.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-tickets\/2.4.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "368",
"id": "368",
"1": "itop-portal",
"name": "itop-portal",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-portal-base\/2.7.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-portal-base\/2.7.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "369",
"id": "369",
"1": "itop-change-mgmt",
"name": "itop-change-mgmt",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0\nDepends on module: itop-tickets\/2.0.0",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)\nDepends on module: itop-config-mgmt\/2.2.0\nDepends on module: itop-tickets\/2.0.0",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
}
]

View File

@@ -0,0 +1,50 @@
[
{
"0": "330",
"id": "330",
"1": "iTop",
"name": "iTop",
"2": "3.3.0-dev-svn",
"version": "3.3.0-dev-svn",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nBuilt on $WCNOW$",
"comment": "Done by the setup program\nBuilt on $WCNOW$",
"5": "0",
"parent_id": "0",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "331",
"id": "331",
"1": "mandatory_module",
"name": "mandatory_module",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nMandatory\nVisible (during the setup)",
"comment": "Done by the setup program\nMandatory\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "yes"
},
{
"0": "332",
"id": "332",
"1": "optional_module",
"name": "optional_module",
"2": "3.3.0",
"version": "3.3.0",
"3": "2025-11-10 11:50:12",
"installed": "2025-11-10 11:50:12",
"4": "Done by the setup program\nOptional\nVisible (during the setup)",
"comment": "Done by the setup program\nOptional\nVisible (during the setup)",
"5": "330",
"parent_id": "330",
"6": "yes",
"uninstallable": "no"
}
]