Compare commits

..

1 Commits

Author SHA1 Message Date
Timothee
c64320b317 N°9009 Add phpunit test to GetSelectedModules 2026-01-07 17:02:42 +01:00
52 changed files with 1128 additions and 2178 deletions

View File

@@ -2685,13 +2685,14 @@ class Config
*
* @param array $aParamValues
* @param ?string $sModulesDir
* @param bool $bPreserveModuleSettings
*
* @return void The current object is modified directly
*
* @throws \Exception
* @throws \CoreException
*/
public function UpdateFromParams($aParamValues, $sModulesDir = null)
public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false)
{
if (isset($aParamValues['application_path'])) {
$this->Set('app_root_url', $aParamValues['application_path']);
@@ -2739,10 +2740,7 @@ class Config
} else {
$aSelectedModules = null;
}
if (! is_null($sModulesDir)) {
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
}
$this->UpdateIncludes($sModulesDir, $aSelectedModules);
if (isset($aParamValues['source_dir'])) {
$this->Set('source_dir', $aParamValues['source_dir']);
@@ -2760,8 +2758,12 @@ class Config
*
* @throws Exception
*/
public function UpdateIncludes(string $sModulesDir, $aSelectedModules = null)
public function UpdateIncludes($sModulesDir, $aSelectedModules = null)
{
if ($sModulesDir === null) {
return;
}
// Initialize the arrays below with default values for the application...
$oEmptyConfig = new Config('dummy_file', false); // Do NOT load any config file, just set the default values
$aAddOns = $oEmptyConfig->GetAddOns();

File diff suppressed because one or more lines are too long

View File

@@ -605,7 +605,6 @@ body {
color:#a00000;
}
.setup-extension-tag {
display: inline-flex;
background-color: grey;
border-radius: 8px;
padding-left: 3px;

View File

@@ -10,7 +10,6 @@ namespace Combodo\iTop\DBTools\Service;
use CMDBSource;
use DBObjectSearch;
use DBObjectSet;
use IssueLog;
class DBToolsUtils
{

View File

@@ -92,7 +92,7 @@ final class CoreUpdater
$sFinalEnv = 'production';
$oRuntimeEnv = new RunTimeEnvironmentCoreUpdater($sFinalEnv, false);
$oRuntimeEnv->CheckDirectories($sFinalEnv);
$oRuntimeEnv->CompileFrom($sFinalEnv);
$oRuntimeEnv->CompileFrom('production');
$oRuntimeEnv->Rollback();
@@ -155,13 +155,21 @@ final class CoreUpdater
APPROOT.'extensions',
];
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $aDirsToScanForModules);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
$aSelectedModules = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
} else {
$aSelectedModules[] = $sModuleId;
}
}
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
@@ -179,7 +187,7 @@ final class CoreUpdater
$oRuntimeEnv->RecordInstallation(
$oConfig,
$sDataModelVersion,
array_keys($aAvailableModules),
$aSelectedModules,
$aSelectedExtensionCodes,
'Done by the iTop Core Updater'
);

View File

@@ -24,13 +24,129 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\HubConnector\Controller\HubController;
use Combodo\iTop\Application\WebPage\JsonPage;
require_once(APPROOT.'application/utils.inc.php');
require_once(APPROOT.'core/log.class.inc.php');
IssueLog::Enable(APPROOT.'log/error.log');
require_once(__DIR__.'/src/Controller/HubController.php');
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'core/dict.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(__DIR__.'/hubruntimeenvironment.class.inc.php');
/**
* Overload of DBBackup to handle logging
*/
class DBBackupWithErrorReporting extends DBBackup
{
protected $aInfos = [];
protected $aErrors = [];
protected function LogInfo($sMsg)
{
$aInfos[] = $sMsg;
}
protected function LogError($sMsg)
{
IssueLog::Error($sMsg);
$aErrors[] = $sMsg;
}
public function GetInfos()
{
return $this->aInfos;
}
public function GetErrors()
{
return $this->aErrors;
}
}
/**
*
* @param string $sTargetFile
* @throws Exception
* @return DBBackupWithErrorReporting
*/
function DoBackup($sTargetFile)
{
// Make sure the target directory exists
$sBackupDir = dirname($sTargetFile);
SetupUtils::builddir($sBackupDir);
$oBackup = new DBBackupWithErrorReporting();
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
$oMutex->Lock();
try {
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
} catch (Exception $e) {
$oMutex->Unlock();
throw $e;
}
$oMutex->Unlock();
return $oBackup;
}
/**
* Outputs the status of the current ajax execution (as a JSON structure)
*
* @param string $sMessage
* @param bool $bSuccess
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
{
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
$oPage = new JsonPage();
$aResult = [
'code' => $iErrorCode,
'message' => $sMessage,
'fields' => $aMoreFields,
];
$oPage->SetData($aResult);
$oPage->SetOutputDataOnly(true);
$oPage->output();
}
/**
* Helper to output the status of a successful execution
*
* @param string $sMessage
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportSuccess($sMessage, $aMoreFields = [])
{
ReportStatus($sMessage, true, 0, $aMoreFields);
}
/**
* Helper to output the status of a failed execution
*
* @param string $sMessage
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
{
if ($iErrorCode == 0) {
// 0 means no error, so change it if no meaningful error code is supplied
$iErrorCode = -1;
}
ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
}
try {
SetupUtils::ExitMaintenanceMode(false); // Reset maintenance mode in case of problem
@@ -67,7 +183,7 @@ try {
foreach ($aChecks as $oCheckResult) {
if ($oCheckResult->iSeverity == CheckResult::ERROR) {
$bFailed = true;
HubController::GetInstance()->ReportError($oCheckResult->sLabel, -2);
ReportError($oCheckResult->sLabel, -2);
}
}
if (!$bFailed) {
@@ -75,27 +191,169 @@ try {
$fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath);
if ($fFreeSpace !== false) {
$sMessage = Dict::Format('iTopHub:BackupFreeDiskSpaceIn', SetupUtils::HumanReadableSize($fFreeSpace), dirname($sDBBackupPath));
HubController::GetInstance()->ReportSuccess($sMessage);
ReportSuccess($sMessage);
} else {
HubController::GetInstance()->ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1);
}
}
break;
case 'do_backup':
HubController::GetInstance()->LaunchBackup();
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
try {
if (MetaModel::GetConfig()->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
SetupLog::Info('Backup starts...');
set_time_limit(0);
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
$iSuffix = 1;
$sSuffix = '';
// Generate a unique name...
do {
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
$sSuffix = '-'.$iSuffix;
$iSuffix++ ;
} while (file_exists($sBackupFile));
$oBackup = DoBackup($sBackupFile);
$aErrors = $oBackup->GetErrors();
if (count($aErrors) > 0) {
SetupLog::Error('Backup failed.');
SetupLog::Error(implode("\n", $aErrors));
ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
} else {
SetupLog::Info('Backup successfully completed.');
ReportSuccess(Dict::S('iTopHub:BackupOk'));
}
} catch (Exception $e) {
SetupLog::Error($e->getMessage());
ReportError($e->getMessage(), $e->getCode());
}
break;
case 'compile':
HubController::GetInstance()->LaunchCompile();
SetupLog::Info('Deployment starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
// First step: prepare the datamodel, if it fails, roll-back
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
if ($oConfig->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
$aSelectModules = $oRuntimeEnv->CompileFrom('production', false); // WARNING symlinks does not seem to be compatible with manual Commit
$oRuntimeEnv->UpdateIncludes($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
// Everything seems Ok so far, commit in env-production!
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
$oRuntimeEnv->Commit();
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Compilation completed...');
ReportSuccess('Ok'); // No access to Dict::S here
break;
case 'move_to_production':
HubController::GetInstance()->LaunchDeploy();
// Second step: update the schema and the data
// Everything happening below is based on env-production
$oRuntimeEnv = new RunTimeEnvironment('production', true);
try {
SetupLog::Info('Move to production starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
unlink(utils::GetDataPath().'hub/compile_authent');
// Load the "production" config file to clone & update it
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
SetupUtils::EnterReadOnlyMode($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$aSelectedModules = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
} else {
$aSelectedModules[] = $sModuleId;
}
}
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
// Record the installation so that the "about box" knows about the installed modules
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
// Plus all "remote" extensions
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
}
}
$aSelectedExtensionCodes = [];
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
$aSelectedExtensionCodes[] = $oExtension->sCode;
}
$aSelectedExtensions = $oExtensionsMap->GetChoices();
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Deployment successfully completed.');
ReportSuccess(Dict::S('iTopHub:CompiledOK'));
} catch (Exception $e) {
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
unlink(utils::GetDataPath().'hub/compile_authent');
}
// Note: at this point, the dictionnary is not necessarily loaded
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
ReportError($e->getMessage(), $e->getCode());
} finally {
SetupUtils::ExitReadOnlyMode();
}
break;
default:
HubController::GetInstance()->ReportError("Invalid operation: '$sOperation'", -1);
ReportError("Invalid operation: '$sOperation'", -1);
}
} catch (Exception $e) {
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
@@ -103,5 +361,5 @@ try {
utils::PopArchiveMode();
HubController::GetInstance()->ReportError($e->getMessage(), $e->getCode());
ReportError($e->getMessage(), $e->getCode());
}

View File

@@ -1,17 +1,9 @@
<?php
namespace Combodo\iTop\HubConnector\setup;
use Config;
use Exception;
use RunTimeEnvironment;
use SetupUtils;
class HubRunTimeEnvironment extends RunTimeEnvironment
{
/**
* Constructor
*
* @param string $sEnvironment
* @param string $bAutoCommit
*/
@@ -32,7 +24,6 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Update the includes for the target environment
*
* @param Config $oConfig
*/
public function UpdateIncludes(Config $oConfig)
@@ -42,9 +33,7 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Move an extension (path to folder of this extension) to the target environment
*
* @param string $sExtensionDirectory The folder of the extension
*
* @throws Exception
*/
public function MoveExtension($sExtensionDirectory)
@@ -68,10 +57,8 @@ class HubRunTimeEnvironment extends RunTimeEnvironment
/**
* Move the selected extensions located in the given directory in data/<target-env>-modules
*
* @param string $sDownloadedExtensionsDir The directory to scan
* @param string[] $aSelectedExtensionDirs The list of folders to move
*
* @throws Exception
*/
public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs)

View File

@@ -186,7 +186,7 @@ function collect_configuration()
// iTop modules
$oConfig = MetaModel::GetConfig();
$aInstalledModules = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
$aInstalledModules = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
foreach ($aInstalledModules as $aDBInfo) {
$aConfiguration['itop_modules'][$aDBInfo['name']] = $aDBInfo['version'];

View File

@@ -1,300 +0,0 @@
<?php
namespace Combodo\iTop\HubConnector\Controller;
use Combodo\iTop\Application\WebPage\JsonPage;
use Combodo\iTop\HubConnector\Model\DBBackupWithErrorReporting;
use Combodo\iTop\HubConnector\setup\HubRunTimeEnvironment;
use Config;
use Dict;
use Exception;
use iTopExtension;
use iTopExtensionsMap;
use iTopMutex;
use LoginWebPage;
use MetaModel;
use MFCompiler;
use RunTimeEnvironment;
use SecurityException;
use SetupLog;
use SetupUtils;
use utils;
require_once(APPROOT.'setup/runtimeenv.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once(APPROOT.'core/mutex.class.inc.php');
require_once(APPROOT.'core/dict.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(__DIR__.'/../setup/hubruntimeenvironment.class.inc.php');
class HubController
{
private static HubController $oInstance;
protected $bOutputHeaders = false;
protected function __construct()
{
}
final public static function GetInstance(): HubController
{
if (!isset(self::$oInstance)) {
self::$oInstance = new HubController();
}
return self::$oInstance;
}
final public static function SetInstance(?HubController $oInstance): void
{
self::$oInstance = $oInstance;
}
public function LaunchBackup()
{
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
try {
if (MetaModel::GetConfig()->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
SetupLog::Info('Backup starts...');
set_time_limit(0);
$sBackupPath = APPROOT.'/data/backups/manual/backup-';
$iSuffix = 1;
$sSuffix = '';
// Generate a unique name...
do {
$sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix;
$sSuffix = '-'.$iSuffix;
$iSuffix++ ;
} while (file_exists($sBackupFile));
$oBackup = $this->DoBackup($sBackupFile);
$aErrors = $oBackup->GetErrors();
if (count($aErrors) > 0) {
SetupLog::Error('Backup failed.');
SetupLog::Error(implode("\n", $aErrors));
$this->ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors);
} else {
SetupLog::Info('Backup successfully completed.');
$this->ReportSuccess(Dict::S('iTopHub:BackupOk'));
}
} catch (Exception $e) {
SetupLog::Error($e->getMessage());
$this->ReportError($e->getMessage(), $e->getCode());
}
}
/**
*
* @param string $sTargetFile
* @throws Exception
* @return DBBackupWithErrorReporting
*/
public function DoBackup($sTargetFile): DBBackupWithErrorReporting
{
// Make sure the target directory exists
$sBackupDir = dirname($sTargetFile);
SetupUtils::builddir($sBackupDir);
$oBackup = new DBBackupWithErrorReporting();
$oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', ''));
$sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
$oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment());
$oMutex->Lock();
try {
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
} catch (Exception $e) {
$oMutex->Unlock();
throw $e;
}
$oMutex->Unlock();
return $oBackup;
}
public function LaunchCompile()
{
SetupLog::Info('Deployment starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
// First step: prepare the datamodel, if it fails, roll-back
$aSelectedExtensionCodes = utils::ReadParam('extension_codes', []);
$aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []);
$oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build
$oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs);
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
if ($oConfig->Get('demo_mode')) {
throw new Exception('Sorry the installation of extensions is not allowed in demo mode');
}
$aSelectModules = $oRuntimeEnv->CompileFrom('production'); // WARNING symlinks does not seem to be compatible with manual Commit
$oRuntimeEnv->UpdateIncludes($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
// Safety check: check the inter dependencies, will throw an exception in case of inconsistency
$oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected
// Everything seems Ok so far, commit in env-production!
$oRuntimeEnv->WriteConfigFileSafe($oConfig);
$oRuntimeEnv->Commit();
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Compilation completed...');
$this->ReportSuccess('Ok'); // No access to Dict::S here
}
public function LaunchDeploy()
{
// Second step: update the schema and the data
// Everything happening below is based on env-production
$oRuntimeEnv = new RunTimeEnvironment('production', true);
try {
SetupLog::Info('Move to production starts...');
$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) {
throw new SecurityException(Dict::S('iTopHub:FailAuthent'));
}
unlink(utils::GetDataPath().'hub/compile_authent');
// Load the "production" config file to clone & update it
$oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE);
SetupUtils::EnterReadOnlyMode($oConfig);
$oRuntimeEnv->InitDataModel($oConfig, true /* model only */);
$aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation');
$oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade');
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation');
$oRuntimeEnv->UpdatePredefinedObjects();
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup');
$oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/);
$oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad');
// Record the installation so that the "about box" knows about the installed modules
$sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion();
$oExtensionsMap = new iTopExtensionsMap();
// Default choices = as before
$oExtensionsMap->LoadChoicesFromDatabase($oConfig);
foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) {
// Plus all "remote" extensions
if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) {
$oExtensionsMap->MarkAsChosen($oExtension->sCode);
}
}
$aSelectedExtensionCodes = [];
foreach ($oExtensionsMap->GetChoices() as $oExtension) {
$aSelectedExtensionCodes[] = $oExtension->sCode;
}
$aSelectedExtensions = $oExtensionsMap->GetChoices();
$oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, array_keys($aAvailableModules), $aSelectedExtensionCodes, 'Done by the iTop Hub Connector');
// Report the success in a way that will be detected by the ajax caller
SetupLog::Info('Deployment successfully completed.');
$this->ReportSuccess(Dict::S('iTopHub:CompiledOK'));
} catch (Exception $e) {
if (file_exists(utils::GetDataPath().'hub/compile_authent')) {
unlink(utils::GetDataPath().'hub/compile_authent');
}
// Note: at this point, the dictionnary is not necessarily loaded
SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage());
SetupLog::Error('Debug trace: '.$e->getTraceAsString());
$this->ReportError($e->getMessage(), $e->getCode());
} finally {
SetupUtils::ExitReadOnlyMode();
}
}
/**
* Outputs the status of the current ajax execution (as a JSON structure)
*
* @param string $sMessage
* @param bool $bSuccess
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = [])
{
// Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation
$this->oLastJsonPage = new JsonPage();
$this->oLastJsonPage->SetOutputHeaders($this->bOutputHeaders);
$aResult = [
'code' => $iErrorCode,
'message' => $sMessage,
'fields' => $aMoreFields,
];
$this->oLastJsonPage->SetData($aResult);
$this->oLastJsonPage->SetOutputDataOnly(true);
$this->oLastJsonPage->output();
}
private ?JsonPage $oLastJsonPage = null;
public function GetLastJsonPage(): ?JsonPage
{
return $this->oLastJsonPage;
}
/**
* Helper to output the status of a successful execution
*
* @param string $sMessage
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportSuccess($sMessage, $aMoreFields = [])
{
$this->ReportStatus($sMessage, true, 0, $aMoreFields);
}
/**
* Helper to output the status of a failed execution
*
* @param string $sMessage
* @param number $iErrorCode
* @param array $aMoreFields
* Extra fields to pass to the caller, if needed
*/
public function ReportError($sMessage, $iErrorCode, $aMoreFields = [])
{
if ($iErrorCode == 0) {
// 0 means no error, so change it if no meaningful error code is supplied
$iErrorCode = -1;
}
$this->ReportStatus($sMessage, false, $iErrorCode, $aMoreFields);
}
/**
* Dont print headers for testing purpose mainly
* @param bool bOutputHeaders
*
* @return void
*/
public function SetOutputHeaders(bool $bOutputHeaders): void
{
$this->bOutputHeaders = $bOutputHeaders;
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace Combodo\iTop\HubConnector\Model;
use DBBackup;
use IssueLog;
/**
* Overload of DBBackup to handle logging
*/
class DBBackupWithErrorReporting extends DBBackup
{
protected $aInfos = [];
protected $aErrors = [];
protected function LogInfo($sMsg)
{
$this->aInfos[] = $sMsg;
}
protected function LogError($sMsg)
{
IssueLog::Error($sMsg);
$this->aErrors[] = $sMsg;
}
public function GetInfos(): array
{
return $this->aInfos;
}
public function GetErrors(): array
{
return $this->aErrors;
}
}

View File

@@ -12,7 +12,8 @@ SetupWebPage::AddModule(
// Setup
//
'dependencies' => [
'itop-structure/2.7.1 || itop-portal/3.0.0', // itop-portal : module_design_itop_design->module_designs->itop-portal
'itop-structure/2.7.1',
'itop-portal/3.0.0', // module_design_itop_design->module_designs->itop-portal
],
'mandatory' => false,
'visible' => true,

View File

@@ -1,11 +1,12 @@
<?php
require_once __DIR__.'/ModuleInstallationRepository.php';
require_once __DIR__.'/ModuleInstallationService.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
private ?array $aSelectInstall = null;
protected function __construct()
{
@@ -22,7 +23,7 @@ class AnalyzeInstallation
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
@@ -57,7 +58,6 @@ class AnalyzeInstallation
* )
* @throws \Exception
*/
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
{
$aRes = [
@@ -96,7 +96,7 @@ class AnalyzeInstallation
$aRes[$sModuleName] = $aModuleInfo;
}
$aCurrentlyInstalledModules = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
$aCurrentlyInstalledModules = ModuleInstallationService::GetInstance()->ReadComputeInstalledModules($oConfig);
// Adjust the list of proposed modules
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {

View File

@@ -1,25 +1,25 @@
<?php
class ModuleInstallationRepository
class ModuleInstallationService
{
private static ModuleInstallationRepository $oInstance;
private static ModuleInstallationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationRepository
final public static function GetInstance(): ModuleInstallationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationRepository();
self::$oInstance = new ModuleInstallationService();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationRepository $oInstance): void
final public static function SetInstance(?ModuleInstallationService $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
@@ -95,17 +95,8 @@ SQL;
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
} catch (MySQLException $e) {
// No database or erroneous information
SetupLog::Error(
'Can not connect to the database',
null,
[
'host' => $oConfig->Get('db_host'),
'user' => $oConfig->Get('db_user'),
'pwd:' => $oConfig->Get('db_pwd'),
'db name' => $oConfig->Get('db_name'),
'msg' => $e->getMessage(),
]
);
$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;
}
@@ -138,10 +129,8 @@ SQL;
// so assume that the datamodel version is equal to the application version
$aResult['datamodel_version'] = $aResult['product_version'];
}
SetupLog::Info(__METHOD__, null, ["product_name" => $aResult['product_name'], "product_version" => $aResult['product_version']]);
return count($aResult) == 0 ? false : $aResult;
$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
@@ -192,47 +181,4 @@ SQL;
return $aInstallByModule;
}
/**
* Return previous module installation. offset is applied on parent_id.
* @param $iOffset: by default (offset=0) returns current installation
* @return array
*/
public static function GetPreviousModuleInstallationsByOffset(int $iOffset = 0): array
{
$oFilter = DBObjectSearch::FromOQL('SELECT ModuleInstallation AS mi WHERE mi.parent_id=0 AND mi.name!="datamodel"');
$oSet = new DBObjectSet($oFilter, ['installed' => false]); // Most recent first
$oSet->SetLimit($iOffset + 1);
$iParentId = 0;
while (!is_null($oModuleInstallation = $oSet->Fetch())) {
/** @var \DBObject $oModuleInstallation */
if ($iOffset == 0) {
$iParentId = $oModuleInstallation->Get('id');
break;
}
$iOffset--;
}
if ($iParentId === 0) {
IssueLog::Error("no ITOP_APPLICATION ModuleInstallation found", null, ['offset' => $iOffset]);
throw new \Exception("no ITOP_APPLICATION ModuleInstallation found");
}
$oFilter = DBObjectSearch::FromOQL("SELECT ModuleInstallation AS mi WHERE mi.id=$iParentId OR mi.parent_id=$iParentId");
$oSet = new DBObjectSet($oFilter); // Most recent first
$aRawValues = $oSet->ToArrayOfValues();
$aValues = [];
foreach ($aRawValues as $aRawValue) {
$aValue = [];
foreach ($aRawValue as $sAliasAttCode => $sValue) {
// remove 'mi.' from AttCode
$sAttCode = substr($sAliasAttCode, 3);
$aValue[$sAttCode] = $sValue;
}
$aValues[] = $aValue;
}
return $aValues;
}
}

View File

@@ -127,7 +127,7 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
/**
* Main program
*/
$sOperation = utils::ReadParam('operation', '');
$sOperation = Utils::ReadParam('operation', '');
try {
SetupUtils::CheckSetupToken();
@@ -164,7 +164,7 @@ try {
break;
case 'toggle_use_symbolic_links':
$sUseSymbolicLinks = utils::ReadParam('bUseSymbolicLinks', false);
$sUseSymbolicLinks = Utils::ReadParam('bUseSymbolicLinks', false);
$bUseSymbolicLinks = ($sUseSymbolicLinks === 'true');
MFCompiler::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
echo "toggle useSymbolicLinks flag : $bUseSymbolicLinks";

View File

@@ -17,13 +17,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
require_once APPROOT.'setup/feature_removal/InplaceSetupAudit.php';
/**
* The base class for the installation process.
@@ -50,8 +46,6 @@ class ApplicationInstaller
protected $oParams;
protected static $bMetaModelStarted = false;
protected Config $oConfig;
/**
* @param \Parameters $oParams
*
@@ -63,9 +57,9 @@ class ApplicationInstaller
$this->oParams = $oParams;
$aParamValues = $oParams->GetParamForConfigArray();
$this->oConfig = new Config();
$this->oConfig->UpdateFromParams($aParamValues);
utils::SetConfig($this->oConfig);
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, null);
utils::SetConfig($oConfig);
}
/**
@@ -221,7 +215,7 @@ class ApplicationInstaller
$aPreinstall = $this->oParams->Get('preinstall');
$aCopies = $aPreinstall['copies'] ?? [];
$this->DoCopy($aCopies);
self::DoCopy($aCopies);
$sReport = "Copying...";
$aResult = [
@@ -244,8 +238,11 @@ class ApplicationInstaller
// __DB__-%Y-%m-%d
$sDestination = $aPreinstall['backup']['destination'];
$sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
$aDBParams = $this->oParams->GetParamForConfigArray();
$oTempConfig = new Config();
$oTempConfig->UpdateFromParams($aDBParams);
$sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
$this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
self::DoBackup($oTempConfig, $sDestination, $sSourceConfigFile, $sMySQLBinDir);
$aResult = [
'status' => self::OK,
@@ -260,9 +257,9 @@ class ApplicationInstaller
$aSelectedModules = $this->oParams->Get('selected_modules');
$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aMiscOptions = $this->oParams->Get('options', []);
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
$sSkipDataAudit = $this->oParams->Get('skip-data-audit', '');
$bUseSymbolicLinks = null;
if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
@@ -275,49 +272,40 @@ class ApplicationInstaller
}
$aParamValues = $this->oParams->GetParamForConfigArray();
$bIsSetupDataAuditEnabled = $this->IsSetupDataAuditEnabled($sSkipDataAudit, $aParamValues);
$this->DoCompile(
$aRemovedExtensionCodes,
self::DoCompile(
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
$bIsSetupDataAuditEnabled,
$bUseSymbolicLinks
$sTargetDir,
$sTargetEnvironment,
$bUseSymbolicLinks,
$aParamValues
);
if ($bIsSetupDataAuditEnabled) {
$sNextStep = 'setup-audit';
$sNextStepLabel = 'Checking data consistency with the new data model';
} else {
$sNextStep = 'db-schema';
$sNextStepLabel = 'Updating database schema';
}
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => $sNextStep,
'next-step-label' => $sNextStepLabel,
'percentage-completed' => 40,
];
break;
case 'setup-audit':
$this->DoSetupAudit();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'db-schema',
'next-step-label' => 'Updating database schema',
'percentage-completed' => 50,
'percentage-completed' => 40,
];
break;
case 'db-schema':
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$bOldAddon = $this->oParams->Get('old_addon', false);
$sUrl = $this->oParams->Get('url', '');
$this->DoUpdateDBSchema(
$aSelectedModules
self::DoUpdateDBSchema(
$aSelectedModules,
$sTargetDir,
$aParamValues,
$sTargetEnvironment,
$bOldAddon,
$sUrl
);
$aResult = [
@@ -330,17 +318,25 @@ class ApplicationInstaller
break;
case 'after-db-create':
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$aAdminParams = $this->oParams->Get('admin_account');
$sAdminUser = $aAdminParams['user'];
$sAdminPwd = $aAdminParams['pwd'];
$sAdminLanguage = $aAdminParams['language'];
$aSelectedModules = $this->oParams->Get('selected_modules', []);
$bOldAddon = $this->oParams->Get('old_addon', false);
$this->AfterDBCreate(
self::AfterDBCreate(
$sTargetDir,
$aParamValues,
$sAdminUser,
$sAdminPwd,
$sAdminLanguage,
$aSelectedModules
$aSelectedModules,
$sTargetEnvironment,
$bOldAddon
);
$aResult = [
@@ -354,10 +350,18 @@ class ApplicationInstaller
case 'load-data':
$aSelectedModules = $this->oParams->Get('selected_modules');
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
$bOldAddon = $this->oParams->Get('old_addon', false);
$bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
$this->DoLoadFiles(
self::DoLoadFiles(
$aSelectedModules,
$sTargetDir,
$aParamValues,
$sTargetEnvironment,
$bOldAddon,
$bSampleData
);
@@ -371,16 +375,24 @@ class ApplicationInstaller
break;
case 'create-config':
$sTargetEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
$sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
$sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
$bOldAddon = $this->oParams->Get('old_addon', false);
$aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
$aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
$aParamValues = $this->oParams->GetParamForConfigArray();
$this->DoCreateConfig(
self::DoCreateConfig(
$sTargetDir,
$sPreviousConfigFile,
$sTargetEnvironment,
$sDataModelVersion,
$bOldAddon,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$aParamValues,
$sInstallComment
);
@@ -461,7 +473,7 @@ class ApplicationInstaller
SetupUtils::ExitReadOnlyMode();
}
protected function DoCopy($aCopies)
protected static function DoCopy($aCopies)
{
$aReports = [];
foreach ($aCopies as $aCopy) {
@@ -482,6 +494,7 @@ class ApplicationInstaller
}
/**
* @param Config $oConfig
* @param string $sBackupFileFormat
* @param string $sSourceConfigFile
* @param string $sMySQLBinDir
@@ -491,25 +504,26 @@ class ApplicationInstaller
* @throws \MySQLException
* @since 2.5.0 uses a {@link Config} object to store DB parameters
*/
protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
protected static function DoBackup($oConfig, $sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
{
$oBackup = new SetupDBBackup($this->oConfig);
$oBackup = new SetupDBBackup($oConfig);
$sTargetFile = $oBackup->MakeName($sBackupFileFormat);
if (!empty($sMySQLBinDir)) {
$oBackup->SetMySQLBinDir($sMySQLBinDir);
}
CMDBSource::InitFromConfig($this->oConfig);
CMDBSource::InitFromConfig($oConfig);
$oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile);
}
/**
* @param array $aRemovedExtensionCodes
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
* @param bool $bIsSetupDataAuditEnabled
* @param string $sTargetDir
* @param string $sEnvironment
* @param boolean $bUseSymbolicLinks
* @param array $aParamValues
*
* @return void
* @throws \ConfigException
@@ -517,24 +531,14 @@ class ApplicationInstaller
*
* @since 3.1.0 N°2013 added the aParamValues param
*/
protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, bool &$bIsSetupDataAuditEnabled, $bUseSymbolicLinks = null)
protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sEnvironment, $bUseSymbolicLinks = null, $aParamValues = [])
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
SetupLog::Info("Compiling data model.");
require_once(APPROOT.'setup/modulediscovery.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
require_once(APPROOT.'setup/compiler.class.inc.php');
$aParamValues = $this->oParams->GetParamForConfigArray();
$sEnvironment = $this->GetTargetEnv();
$sTargetDir = $this->GetTargetDir();
if (empty($sSourceDir) || empty($sTargetDir)) {
throw new Exception("missing parameter source_dir and/or target_dir");
}
@@ -556,25 +560,20 @@ class ApplicationInstaller
if (!is_dir($sSourcePath)) {
throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server");
}
$bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode();
if ($bIsSetupDataAuditEnabled) {
if ($bIsAlreadyInMaintenanceMode) {
//required to read DM before calling SaveModelInfo
SetupUtils::ExitMaintenanceMode();
$bIsAlreadyInMaintenanceMode = false;
}
$bIsSetupDataAuditEnabled = $this->SaveModelInfo($sEnvironment);
}
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
if (is_file($sConfigFilePath)) {
$oConfig = new Config($sConfigFilePath);
$oConfig->UpdateFromParams($aParamValues);
SetupUtils::EnterMaintenanceMode($oConfig);
} else {
$oConfig = null;
}
if (false === is_null($oConfig)) {
$oConfig->UpdateFromParams($aParamValues);
}
SetupUtils::EnterMaintenanceMode($oConfig);
}
if (!is_dir($sTargetPath)) {
@@ -590,9 +589,6 @@ class ApplicationInstaller
SetupUtils::tidydir($sTargetPath);
}
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$oFactory = new ModelFactory($aDirsToScan);
$oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries');
@@ -659,92 +655,20 @@ class ApplicationInstaller
}
}
private function GetModelInfoPath(string $sEnv): string
{
return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
}
private function SaveModelInfo(string $sEnvironment): bool
{
$sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
try {
$aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
} catch (Exception $e) {
//logged already
return is_file($sModelInfoPath);
}
return (bool) file_put_contents($sModelInfoPath, json_encode($aModelInfo));
}
private function GetPreviousModelInfo(string $sEnvironment): array
{
$sContent = file_get_contents($this->GetModelInfoPath($sEnvironment));
$aModelInfo = json_decode($sContent, true);
if (false === $aModelInfo) {
throw new Exception("Could not read (before compilation) previous model to audit data");
}
return $aModelInfo;
}
protected function DoSetupAudit()
{
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
*/
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
$sTargetEnvironment = $this->GetTargetEnv();
$aPreviousCompilationModelInfo = $this->GetPreviousModelInfo($sTargetEnvironment);
$oSetupAudit = new InplaceSetupAudit($aPreviousCompilationModelInfo, $sTargetEnvironment);
$oSetupAudit->GetIssues(true);
$iCount = $oSetupAudit->GetDataToCleanupCount();
if ($iCount > 0) {
throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop");
}
}
private function IsSetupDataAuditEnabled($sSkipDataAudit, array $aParamValues): bool
{
if ($sSkipDataAudit === "checked") {
SetupLog::Info("Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
return false;
}
$sMode = $aParamValues['mode'];
if ($sMode !== "upgrade") {
//first install
return false;
}
$sPath = APPROOT.$this->GetTargetDir();
if (!is_dir($sPath)) {
SetupLog::Info("Reinstallation of an iTop from a backup (No ".$this->GetTargetDir()." found). Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
return false;
}
return true;
}
/**
* @param $aSelectedModules
* @param $sModulesDir
* @param $aParamValues
* @param string $sTargetEnvironment
* @param bool $bOldAddon
* @param string $sAppRootUrl
*
* @throws \ConfigException
* @throws \CoreException
* @throws \MySQLException
*/
protected function DoUpdateDBSchema($aSelectedModules)
protected static function DoUpdateDBSchema($aSelectedModules, $sModulesDir, $aParamValues, $sTargetEnvironment = '', $bOldAddon = false, $sAppRootUrl = '')
{
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
$aParamValues = $this->oParams->GetParamForConfigArray();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
@@ -758,6 +682,11 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model only
@@ -812,8 +741,9 @@ class ApplicationInstaller
}
// Module specific actions (migrate the data)
//
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation');
if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
@@ -909,16 +839,16 @@ class ApplicationInstaller
ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
}
protected function AfterDBCreate(
protected static function AfterDBCreate(
$sModulesDir,
$aParamValues,
$sAdminUser,
$sAdminPwd,
$sAdminLanguage,
$aSelectedModules
$aSelectedModules,
$sTargetEnvironment,
$bOldAddon
) {
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
@@ -930,6 +860,11 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
$oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
$oContextTag = new ContextTag(ContextTag::TAG_SETUP);
@@ -938,7 +873,7 @@ class ApplicationInstaller
// Perform here additional DB setup... profiles, etc...
//
$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation');
$oProductionEnv->UpdatePredefinedObjects();
@@ -952,7 +887,7 @@ class ApplicationInstaller
// Perform final setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup');
}
/**
@@ -970,14 +905,14 @@ class ApplicationInstaller
}
}
protected function DoLoadFiles(
protected static function DoLoadFiles(
$aSelectedModules,
$sModulesDir,
$aParamValues,
$sTargetEnvironment = 'production',
$bOldAddon = false,
$bSampleData = false
) {
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
@@ -987,6 +922,11 @@ class ApplicationInstaller
$oConfig = new Config();
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
//Load the MetaModel if needed (asynchronous mode)
@@ -997,19 +937,22 @@ class ApplicationInstaller
}
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
$oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
$oProductionEnv->LoadData($aAvailableModules, $aSelectedModules, $bSampleData);
// Perform after dbload setup tasks here
//
$oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
$oProductionEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad');
}
/**
* @param string $sModulesDir
* @param string $sPreviousConfigFile
* @param string $sTargetEnvironment
* @param string $sDataModelVersion
* @param boolean $bOldAddon
* @param array $aSelectedModuleCodes
* @param array $aSelectedExtensionCodes
* @param string|null $sInstallComment
* @param array $aParamValues parameters array used to create config file using {@see Config::UpdateFromParams}
*
* @param null $sInstallComment
*
@@ -1017,17 +960,17 @@ class ApplicationInstaller
* @throws \CoreException
* @throws \Exception
*/
protected function DoCreateConfig(
protected static function DoCreateConfig(
$sModulesDir,
$sPreviousConfigFile,
$sTargetEnvironment,
$sDataModelVersion,
$bOldAddon,
$aSelectedModuleCodes,
$aSelectedExtensionCodes,
$aParamValues,
$sInstallComment = null
) {
$aParamValues = $this->oParams->GetParamForConfigArray();
$sTargetEnvironment = $this->GetTargetEnv();
$sModulesDir = $this->GetTargetDir();
/**
* @since 3.2.0 move the ContextTag init at the very beginning of the method
* @noinspection PhpUnusedLocalVariableInspection
@@ -1037,10 +980,12 @@ class ApplicationInstaller
$aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
$sMode = $aParamValues['mode'];
$bPreserveModuleSettings = false;
if ($sMode == 'upgrade') {
try {
$oOldConfig = new Config($sPreviousConfigFile);
$oConfig = clone($oOldConfig);
$bPreserveModuleSettings = true;
} catch (Exception $e) {
// In case the previous configuration is corrupted... start with a blank new one
$oConfig = new Config();
@@ -1054,7 +999,11 @@ class ApplicationInstaller
$oConfig->Set('access_mode', ACCESS_FULL);
// Final config update: add the modules
$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
$oConfig->UpdateFromParams($aParamValues, $sModulesDir, $bPreserveModuleSettings);
if ($bOldAddon) {
// Old version of the add-on for backward compatibility with pre-2.0 data models
$oConfig->SetAddons([]);
}
// Record which modules are installed...
$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);

View File

@@ -21,8 +21,8 @@
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\Page;
use Combodo\iTop\DesignDocument;
use Combodo\iTop\DesignElement;
use Combodo\iTop\DesignDocument;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
@@ -3359,8 +3359,6 @@ EOF;
$bDataXmlPrecompiledFileExists = false;
clearstatcache();
$iDataXmlFileLastModified = 0;
if (!empty($sPrecompiledFileUri)) {
$sDataXmlProvidedPrecompiledFile = $sTempTargetDir.DIRECTORY_SEPARATOR.$sPrecompiledFileUri;
$bDataXmlPrecompiledFileExists = file_exists($sDataXmlProvidedPrecompiledFile) ;

View File

@@ -34,7 +34,7 @@ require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
$sOperation = utils::ReadParam('operation', 'step1');
$sOperation = Utils::ReadParam('operation', 'step1');
$oP = new SetupPage('iTop email test utility');
// Although this page doesn't expose sensitive info, with it we can send multiple emails
@@ -208,7 +208,7 @@ function DisplayStep2(SetupPage $oP, $sFrom, $sTo)
$oP->add("<p>Sending an email to '".htmlentities($sTo, ENT_QUOTES, 'utf-8')."'... (From: '".htmlentities($sFrom, ENT_QUOTES, 'utf-8')."')</p>\n");
$oP->add("<form method=\"post\">\n");
$oEmail = new EMail();
$oEmail = new Email();
$oEmail->SetRecipientTO($sTo);
$oEmail->SetRecipientFrom($sFrom);
$oEmail->SetSubject("Test iTop");
@@ -256,8 +256,8 @@ try {
case 'step2':
$oP->no_cache();
$sTo = utils::ReadParam('to', '', false, 'raw_data');
$sFrom = utils::ReadParam('from', '', false, 'raw_data');
$sTo = Utils::ReadParam('to', '', false, 'raw_data');
$sFrom = Utils::ReadParam('from', '', false, 'raw_data');
DisplayStep2($oP, $sFrom, $sTo);
break;

View File

@@ -3,11 +3,143 @@
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/setup/itopextension.class.inc.php');
require_once(APPROOT.'/setup/parameters.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
/**
* Basic helper class to describe an extension, with some characteristics and a list of modules
*/
class iTopExtension
{
public const SOURCE_WIZARD = 'datamodels';
public const SOURCE_MANUAL = 'extensions';
public const SOURCE_REMOTE = 'data';
/**
* @var string
*/
public $sCode;
/**
* @var string
*/
public $sVersion;
/**
* @var string
*/
public $sInstalledVersion;
/**
* @var string
*/
public $sLabel;
/**
* @var string
*/
public $sDescription;
/**
* @var string
*/
public $sSource;
/**
* @var bool
*/
public $bMandatory;
/**
* @var string
*/
public $sMoreInfoUrl;
/**
* @var bool
*/
public $bMarkedAsChosen;
/**
* If null, check if at least one module cannot be uninstalled
* @var bool|null
*/
public ?bool $bCanBeUninstalled = null;
/**
* @var bool
*/
public $bVisible;
/**
* @var string[]
*/
public $aModules;
/**
* @var string[]
*/
public $aModuleVersion;
/**
* @var string[]
*/
public $aModuleInfo;
/**
* @var string
*/
public $sSourceDir;
/**
*
* @var string[]
*/
public $aMissingDependencies;
/**
* @var bool
*/
public bool $bInstalled = false;
/**
* @var bool
*/
public bool $bRemovedFromDisk = false;
public function __construct()
{
$this->sCode = '';
$this->sLabel = '';
$this->sDescription = '';
$this->sSource = self::SOURCE_WIZARD;
$this->bMandatory = false;
$this->sMoreInfoUrl = '';
$this->bMarkedAsChosen = false;
$this->sVersion = ITOP_VERSION;
$this->sInstalledVersion = '';
$this->aModules = [];
$this->aModuleVersion = [];
$this->aModuleInfo = [];
$this->sSourceDir = '';
$this->bVisible = true;
$this->aMissingDependencies = [];
}
/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled(): bool
{
if (!is_null($this->bCanBeUninstalled)) {
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
$this->bCanBeUninstalled = $aModuleInfo['uninstallable'] === 'yes';
return $this->bCanBeUninstalled;
}
return true;
}
}
/**
* Helper class to discover all available extensions on a given iTop system
@@ -176,26 +308,28 @@ class iTopExtensionsMap
return $this->aExtensionsByCode[$sExtensionCode] ?? null;
}
/**
* @param array<string> $aExtensionCodes
* @return void
*/
public function DeclareExtensionAsRemoved(array $aExtensionCodes): void
/*public function GetMissingExtensions(array $aSelectedExtensions)
{
$aRemovedExtension = [];
foreach ($aExtensionCodes as $sCode) {
/** @var \iTopExtension $oExtension */
$oExtension = $this->GetFromExtensionCode($sCode);
if (!is_null($oExtension)) {
$aRemovedExtension [] = $oExtension;
\IssueLog::Info(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]);
\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 {
\IssueLog::Warning(__METHOD__." cannot find extensions", null, ['code' => $sCode]);
\SetupLog::Info(__METHOD__." MISSING $sExtensionCode");
}
}
\SetupLog::Info(__METHOD__, null, $aRes);
\ModuleDiscovery::DeclareRemovedExtensions($aRemovedExtension);
}
return $aRes;
}*/
/**
* Read (recursively) a directory to find if it contains extensions (or modules)
@@ -369,75 +503,6 @@ class iTopExtensionsMap
return array_merge($this->aInstalledExtensions ?? [], $this->aExtensions);
}
/**
* @param bool $bKeepMissingDependencyExtensions
*
* @return array<\iTopExtension>>
*/
public function GetAllExtensionsToDisplayInSetup(bool $bKeepMissingDependencyExtensions = false): array
{
$aRes = [];
foreach ($this->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
/** @var \iTopExtension $oExtension */
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible)) {
if ($bKeepMissingDependencyExtensions || (count($oExtension->aMissingDependencies) == 0)) {
if (!$oExtension->bMandatory) {
$oExtension->bMandatory = ($oExtension->sSource === iTopExtension::SOURCE_REMOTE);
}
$aRes[$oExtension->sCode] = $oExtension;
}
}
}
return $aRes;
}
public function GetAllExtensionsOptionInfo(): array
{
$aRes = [];
foreach ($this->GetAllExtensionsToDisplayInSetup() as $sCode => $oExtension) {
$aRes[] = [
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory,
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
];
}
return $aRes;
}
protected function GetExtensionSourceLabel($sSource)
{
$sDecorationClass = '';
switch ($sSource) {
case iTopExtension::SOURCE_MANUAL:
$sResult = 'Local extensions folder';
$sDecorationClass = 'fas fa-folder';
break;
case iTopExtension::SOURCE_REMOTE:
$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer';
$sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler';
break;
default:
$sResult = '';
}
if ($sResult == '') {
return '';
}
return '<i class="setup-extension--icon '.$sDecorationClass.'" data-tooltip-content="'.$sResult.'"></i>';
}
/**
* Mark the given extension as chosen
* @param string $sExtensionCode The code of the extension (code without version number)
@@ -523,7 +588,7 @@ class iTopExtensionsMap
return true;
}
public function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
protected function LoadInstalledExtensionsFromDatabase(Config $oConfig): array|false
{
try {
if (CMDBSource::DBName() === null) {

View File

@@ -1,105 +0,0 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use DBObjectSearch;
use DBObjectSet;
use IssueLog;
use MetaModel;
use SetupLog;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
abstract class AbstractSetupAudit
{
protected bool $bClassesInitialized = false;
protected array $aClassesBefore = [];
protected array $aClassesAfter = [];
protected array $aRemovedClasses = [];
protected array $aFinalClassesToCleanup = [];
public function __construct()
{
}
abstract public function ComputeClasses(): void;
public function GetRemovedClasses(): array
{
$this->ComputeClasses();
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBefore) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfter) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
foreach ($aClasses as $i => $sClass) {
$this->aRemovedClasses[] = $sClass;
}
}
return $this->aRemovedClasses;
}
public function GetIssues(bool $bStopDataCheckAtFirstIssue = false): array
{
$this->aFinalClassesToCleanup = [];
foreach ($this->GetRemovedClasses() as $sClass) {
if (MetaModel::IsAbstract($sClass)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesToCleanup[$sClass] = $iCount;
if ($bStopDataCheckAtFirstIssue && $iCount > 0) {
//setup envt: should raise issue ASAP
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, $this->aFinalClassesToCleanup);
return $this->aFinalClassesToCleanup;
}
}
}
$this->LogInfoWithProperLogger("Setup audit found data to cleanup", null, ['data_to_cleanup' => $this->aFinalClassesToCleanup]);
return $this->aFinalClassesToCleanup;
}
public function GetDataToCleanupCount(): int
{
$res = 0;
foreach ($this->aFinalClassesToCleanup as $sClass => $iCount) {
$res += $iCount;
}
return $res;
}
private function Count($sClass): int
{
$oSearch = DBObjectSearch::FromOQL("SELECT $sClass", []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
return $oSet->Count();
}
//could be shared with others in log APIs ?
private function LogInfoWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Info($sMessage, $sChannel, $aContext);
} else {
IssueLog::Info($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -2,7 +2,6 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use iTopExtensionsMap;
use MetaModel;
use RunTimeEnvironment;
use SetupUtils;
@@ -12,6 +11,7 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
public const DRY_REMOVAL_AUDIT_ENV = "extension-removal";
protected array $aExtensionsByCode;
private bool $bExtensionMapModified = false;
/**
* Toolset for building a run-time environment
@@ -41,16 +41,29 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
$this->Cleanup();
SetupUtils::copydir(APPROOT."/data/$sSourceEnv-modules", APPROOT."/data/$sEnv-modules");
$this->DeclareExtensionAsRemoved($aExtensionCodesToRemove);
if (count($aExtensionCodesToRemove) > 0) {
$this->RemoveExtensionsLocally($aExtensionCodesToRemove);
}
$oDryRemovalConfig = clone(MetaModel::GetConfig());
$oDryRemovalConfig->ChangeModulesPath($sSourceEnv, $this->sFinalEnv);
$this->WriteConfigFileSafe($oDryRemovalConfig);
}
private function DeclareExtensionAsRemoved(array $aExtensionCodes): void
private function RemoveExtensionsLocally(array $aExtensionCodes): void
{
$oExtensionsMap = new iTopExtensionsMap($this->sFinalEnv);
$oExtensionsMap->DeclareExtensionAsRemoved($aExtensionCodes);
$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()
@@ -62,4 +75,23 @@ class DryRemovalRuntimeEnvironment extends RunTimeEnvironment
SetupUtils::rrmdir(APPROOT."/conf/$sEnv");
@unlink(APPROOT."/data/datamodel-$sEnv.xml");
}
/**
* @return \iTopExtensionsMap|null
*/
/*protected function GetExtensionMap(): ?iTopExtensionsMap
{
if (is_null(parent::GetExtensionMap())) {
return null;
}
if (!$this->bExtensionMapModified) {
$this->bExtensionMapModified = true;
foreach ($this->aExtensionsByCode as $sCode) {
parent::GetExtensionMap()->RemoveExtension($sCode);
}
}
return parent::GetExtensionMap();
}*/
}

View File

@@ -1,40 +0,0 @@
<?php
namespace Combodo\iTop\Setup\FeatureRemoval;
use MetaModel;
require_once __DIR__.'/AbstractSetupAudit.php';
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class InplaceSetupAudit extends AbstractSetupAudit
{
//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 $sEnvAfter;
public function __construct(array $aClassesBefore, string $sEnvAfter)
{
parent::__construct();
$this->aClassesBefore = $aClassesBefore;
$this->sEnvAfter = $sEnvAfter;
}
public function ComputeClasses(): void
{
if ($this->bClassesInitialized) {
return;
}
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvAfter) {
$this->aClassesAfter = MetaModel::GetClasses();
} else {
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
}
$this->bClassesInitialized = true;
}
}

View File

@@ -2,12 +2,8 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use SetupLog;
use utils;
class ModelReflectionSerializer
{
@@ -33,38 +29,27 @@ class ModelReflectionSerializer
public function GetModelFromEnvironment(string $sEnv): array
{
IssueLog::Info(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
\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) {
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput]);
\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) {
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
\IssueLog::Error("Invalid JSON", null, ["output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
throw new Exception("cannot get classes from $sEnv");
\IssueLog::Error("not an array", null, ["classes" => $aClasses]);
throw new Exception("cannot get classes");
}
return $aClasses;
}
//could be shared with others in log APIs ?
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Error($sMessage, $sChannel, $aContext);
} else {
IssueLog::Error($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -2,46 +2,45 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use DBObjectSearch;
use DBObjectSet;
use MetaModel;
require_once __DIR__.'/AbstractSetupAudit.php';
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
class SetupAudit extends AbstractSetupAudit
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 $sEnvBefore;
private string $sEnvAfter;
private string $sEnvBeforeExtensionRemoval;
private string $sEnvAfterExtensionRemoval;
public function __construct(string $sEnvBefore, string $sEnvAfter)
{
parent::__construct();
$this->sEnvBefore = $sEnvBefore;
$this->sEnvAfter = $sEnvAfter;
}
private array $aClassesBeforeRemoval;
private array $aClassesAfterRemoval;
private array $aRemovedClasses;
private array $aFinalClassesRemoved;
public function ComputeClasses(): void
public function __construct(string $sEnvBeforeExtensionRemoval, string $sEnvAfterExtensionRemoval = DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV)
{
if ($this->bClassesInitialized) {
return;
}
$this->sEnvBeforeExtensionRemoval = $sEnvBeforeExtensionRemoval;
$this->sEnvAfterExtensionRemoval = $sEnvAfterExtensionRemoval;
$sCurrentEnvt = MetaModel::GetEnvironment();
if ($sCurrentEnvt === $this->sEnvBefore) {
$this->aClassesBefore = MetaModel::GetClasses();
if ($sCurrentEnvt === $this->sEnvBeforeExtensionRemoval) {
$this->aClassesBeforeRemoval = MetaModel::GetClasses();
} else {
$this->aClassesBefore = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBefore);
$this->aClassesBeforeRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvBeforeExtensionRemoval);
}
if ($sCurrentEnvt === $this->sEnvAfter) {
$this->aClassesAfter = MetaModel::GetClasses();
if ($sCurrentEnvt === $this->sEnvAfterExtensionRemoval) {
$this->aClassesAfterRemoval = MetaModel::GetClasses();
} else {
$this->aClassesAfter = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfter);
$this->aClassesAfterRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->sEnvAfterExtensionRemoval);
}
$this->bClassesInitialized = true;
$this->aRemovedClasses = [];
$this->aFinalClassesRemoved = [];
}
/*public function SetSelectedExtensions(Config $oConfig, array $aSelectedExtensions)
@@ -57,18 +56,16 @@ class SetupAudit extends AbstractSetupAudit
public function GetRemovedClasses(): array
{
$this->ComputeClasses();
if (count($this->aRemovedClasses) == 0) {
if (count($this->aClassesBefore) == 0) {
if (count($this->aClassesBeforeRemoval) == 0) {
return $this->aRemovedClasses;
}
if (count($this->aClassesAfter) == 0) {
if (count($this->aClassesAfterRemoval) == 0) {
return $this->aRemovedClasses;
}
$aExtensionsNames = array_diff($this->aClassesBefore, $this->aClassesAfter);
$aExtensionsNames = array_diff($this->aClassesBeforeRemoval, $this->aClassesAfterRemoval);
$this->aRemovedClasses = [];
$aClasses = array_values($aExtensionsNames);
sort($aClasses);
@@ -80,4 +77,50 @@ class SetupAudit extends AbstractSetupAudit
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

@@ -1,144 +0,0 @@
<?php
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'/setup/parameters.class.inc.php');
require_once(APPROOT.'/core/cmdbsource.class.inc.php');
require_once(APPROOT.'/setup/modulediscovery.class.inc.php');
require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
/**
* Basic helper class to describe an extension, with some characteristics and a list of modules
*/
class iTopExtension
{
public const SOURCE_WIZARD = 'datamodels';
public const SOURCE_MANUAL = 'extensions';
public const SOURCE_REMOTE = 'data';
/**
* @var string
*/
public $sCode;
/**
* @var string
*/
public $sVersion;
/**
* @var string
*/
public $sInstalledVersion;
/**
* @var string
*/
public $sLabel;
/**
* @var string
*/
public $sDescription;
/**
* @var string
*/
public $sSource;
/**
* @var bool
*/
public $bMandatory;
/**
* @var string
*/
public $sMoreInfoUrl;
/**
* @var bool
*/
public $bMarkedAsChosen;
/**
* If null, check if at least one module cannot be uninstalled
* @var bool|null
*/
public ?bool $bCanBeUninstalled = null;
/**
* @var bool
*/
public $bVisible;
/**
* @var string[]
*/
public $aModules;
/**
* @var string[]
*/
public $aModuleVersion;
/**
* @var string[]
*/
public $aModuleInfo;
/**
* @var string
*/
public $sSourceDir;
/**
*
* @var string[]
*/
public $aMissingDependencies;
/**
* @var bool
*/
public bool $bInstalled = false;
/**
* @var bool
*/
public bool $bRemovedFromDisk = false;
public function __construct()
{
$this->sCode = '';
$this->sLabel = '';
$this->sDescription = '';
$this->sSource = self::SOURCE_WIZARD;
$this->bMandatory = false;
$this->sMoreInfoUrl = '';
$this->bMarkedAsChosen = false;
$this->sVersion = ITOP_VERSION;
$this->sInstalledVersion = '';
$this->aModules = [];
$this->aModuleVersion = [];
$this->aModuleInfo = [];
$this->sSourceDir = '';
$this->bVisible = true;
$this->aMissingDependencies = [];
}
/**
* @since 3.3.0
* @return bool
*/
public function CanBeUninstalled(): bool
{
if (!is_null($this->bCanBeUninstalled)) {
return $this->bCanBeUninstalled;
}
foreach ($this->aModuleInfo as $sModuleCode => $aModuleInfo) {
if ($aModuleInfo['uninstallable'] !== 'yes') {
return false;
}
}
return true;
}
}

View File

@@ -5,7 +5,7 @@ namespace Combodo\iTop\Setup\ModuleDependency;
require_once(APPROOT.'/setup/runtimeenv.class.inc.php');
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
use ModuleFileReaderException;
use RunTimeEnvironment;
/**
@@ -63,17 +63,17 @@ class DependencyExpression
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return self::$oPhpExpressionEvaluator;
return static::$oPhpExpressionEvaluator;
}
/**
* Return module names potentially required by current dependency
*
* @return array<string>
* @return array
*/
public function GetRemainingModuleNamesToResolve(): array
{

View File

@@ -114,7 +114,7 @@ class Module
}
/**
* @return array<string> list of unique module names
* @return array: list of unique module names
*/
public function GetUnresolvedDependencyModuleNames(): array
{

View File

@@ -19,16 +19,16 @@ class ModuleDependencySort
final public static function GetInstance(): ModuleDependencySort
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleDependencySort();
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
return self::$oInstance;
return static::$oInstance;
}
final public static function SetInstance(?ModuleDependencySort $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
@@ -168,7 +168,7 @@ class ModuleDependencySort
foreach ($aCountDepsByModuleId as $sModuleId => $iInDegreeCounter) {
$oModule = $aUnresolvedDependencyModules[$sModuleId];
if ($bOneLoopAtLeast && ($iInDegreeCounter > 0)) {
if ($bOneLoopAtLeast && $iInDegreeCounter > 0) {
break;
}

View File

@@ -22,13 +22,13 @@
use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException;
require_once(APPROOT.'setup/modulediscovery/ModuleFileReader.php');
require_once(__DIR__.'/moduledependency/moduledependencysort.class.inc.php');
require_once(__DIR__.'/itopextension.class.inc.php');
use Combodo\iTop\Setup\ModuleDependency\ModuleDependencySort;
class MissingDependencyException extends CoreException
{
@@ -95,9 +95,6 @@ class ModuleDiscovery
protected static $m_aModules = [];
protected static $m_aModuleVersionByName = [];
/** @var array<\iTopExtension> $m_aRemovedExtensions */
protected static $m_aRemovedExtensions = [];
// All the entries below are list of file paths relative to the module directory
protected static $m_aFilesList = ['datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample'];
@@ -134,10 +131,6 @@ class ModuleDiscovery
list($sModuleName, $sModuleVersion) = static::GetModuleName($sId);
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $sModuleVersion, $aArgs)) {
return;
}
if (array_key_exists($sModuleName, self::$m_aModuleVersionByName)) {
if (version_compare($sModuleVersion, self::$m_aModuleVersionByName[$sModuleName]['version'], '>')) {
// Newer version, let's upgrade
@@ -221,97 +214,29 @@ class ModuleDiscovery
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
if (is_null($aModulesToLoad) && count(self::$m_aRemovedExtensions) === 0) {
if (is_null($aModulesToLoad)) {
$aFilteredModules = $aModules;
} else {
$aFilteredModules = [];
foreach ($aModules as $sModuleId => $aModuleInfo) {
foreach ($aModules as $sModuleId => $aModule) {
$oModule = new Module($sModuleId);
$sModuleName = $oModule->GetModuleName();
if (self::IsModuleInExtensionList(self::$m_aRemovedExtensions, $sModuleName, $oModule->GetVersion(), $aModuleInfo)) {
continue;
}
if (is_null($aModulesToLoad) || in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModuleInfo;
if (in_array($sModuleName, $aModulesToLoad)) {
$aFilteredModules[$sModuleId] = $aModule;
}
}
}
return ModuleDependencySort::GetInstance()->GetModulesOrderedForInstallation($aFilteredModules, $bAbortOnMissingDependency);
}
/**
* @param array<\iTopExtension> $aRemovedExtension
* @return void
*/
public static function DeclareRemovedExtensions(array $aRemovedExtension)
{
if (self::$m_aRemovedExtensions != $aRemovedExtension) {
self::ResetCache();
}
self::$m_aRemovedExtensions = $aRemovedExtension;
}
/**
* @param array<\iTopExtension> $aExtensions
* @param string $sModuleName
* @param string $sModuleVersion
* @param array $aModuleInfo
*
* @return bool
*/
private static function IsModuleInExtensionList(array $aExtensions, string $sModuleName, string $sModuleVersion, array $aModuleInfo): bool
{
if (count($aExtensions) === 0) {
return false;
}
$aNonMatchingPaths = [];
$sModuleFilePath = $aModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
/** @var \iTopExtension $oExtension */
foreach ($aExtensions as $oExtension) {
$sCurrentVersion = $oExtension->aModuleVersion[$sModuleName] ?? null;
if (is_null($sCurrentVersion)) {
continue;
}
if ($sModuleVersion !== $sCurrentVersion) {
continue;
}
$aCurrentModuleInfo = $oExtension->aModuleInfo[$sModuleName] ?? null;
if (is_null($aCurrentModuleInfo)) {
SetupLog::Warning("Missing $sModuleName in ".$oExtension->sLabel.". it should not happen");
continue;
}
// use case: same module coming from 2 different extensions
// we remove only the one coming from removed extensions
$sCurrentModuleFilePath = $aCurrentModuleInfo[ModuleFileReader::MODULE_FILE_PATH];
if (realpath($sModuleFilePath) !== realpath($sCurrentModuleFilePath)) {
$aNonMatchingPaths[] = $sCurrentModuleFilePath;
continue;
}
SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]);
return true;
}
if (count($aNonMatchingPaths) > 0) {
//add log for support
SetupLog::Debug("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]);
}
return false;
}
private static function GetPhpExpressionEvaluator(): PhpExpressionEvaluator
{
if (!isset(self::$oPhpExpressionEvaluator)) {
self::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
if (!isset(static::$oPhpExpressionEvaluator)) {
static::$oPhpExpressionEvaluator = new PhpExpressionEvaluator([], RunTimeEnvironment::STATIC_CALL_AUTOSELECT_WHITELIST);
}
return self::$oPhpExpressionEvaluator;
return static::$oPhpExpressionEvaluator;
}
/**
@@ -360,12 +285,10 @@ class ModuleDiscovery
/**
* Helper function to interpret the name of a module
*
* @param $sModuleId string Identifier of the module, in the form 'name/version'
*
* @return array of 2 elements (name, version)
* @return array(name, version)
*/
public static function GetModuleName($sModuleId): array
public static function GetModuleName($sModuleId)
{
$aMatches = [];
if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) {

View File

@@ -7,15 +7,15 @@ use CoreException;
use Exception;
use ParseError;
use PhpParser\Error;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use PhpParser\ParserFactory;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Arg;
require_once __DIR__.'/ModuleFileReaderException.php';
require_once APPROOT.'sources/PhpParser/Evaluation/PhpExpressionEvaluator.php';
@@ -36,7 +36,6 @@ class ModuleFileReader
public const MODULE_INFO_PATH = 0;
public const MODULE_INFO_ID = 1;
public const MODULE_INFO_CONFIG = 2;
public const MODULE_FILE_PATH = "module_file_path";
public const STATIC_CALLWHITELIST = [
"utils::GetItopVersionWikiSyntax",
@@ -49,23 +48,21 @@ class ModuleFileReader
final public static function GetInstance(): ModuleFileReader
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleFileReader();
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
}
return self::$oInstance;
return static::$oInstance;
}
final public static function SetInstance(?ModuleFileReader $oInstance): void
{
self::$oInstance = $oInstance;
static::$oInstance = $oInstance;
}
/**
* Read the information from a module file (module.xxx.php)
*
* @param string $sModuleFilePath
*
* @param string $sModuleFile
* @return array
* @throws ModuleFileReaderException
*/
@@ -111,9 +108,7 @@ class ModuleFileReader
* Read the information from a module file (module.xxx.php)
* Warning: this method is using eval() function to load the ModuleInstallerAPI classes.
* Current method is never called at design/runtime. It is acceptable to use it during setup only.
*
* @param string $sModuleFilePath
*
* @param string $sModuleFile
* @return array
* @throws ModuleFileReaderException
*/
@@ -169,7 +164,7 @@ class ModuleFileReader
private function CompleteModuleInfoWithFilePath(array &$aModuleInfo)
{
if (count($aModuleInfo) == 3) {
$aModuleInfo[static::MODULE_INFO_CONFIG][self::MODULE_FILE_PATH] = $aModuleInfo[static::MODULE_INFO_PATH];
$aModuleInfo[static::MODULE_INFO_CONFIG]['module_file_path'] = $aModuleInfo[static::MODULE_INFO_PATH];
}
}
@@ -180,21 +175,15 @@ class ModuleFileReader
}
$sModuleInstallerClass = $aModuleInfo['installer'];
if (strlen($sModuleInstallerClass) === 0) {
return null;
}
if (!class_exists($sModuleInstallerClass)) {
$sModuleFilePath = $aModuleInfo[self::MODULE_FILE_PATH];
$sModuleFilePath = $aModuleInfo['module_file_path'];
$this->ReadModuleFileInformationUnsafe($sModuleFilePath);
}
if (!class_exists($sModuleInstallerClass)) {
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not a PHP class - Module: ".$aModuleInfo['label']);
}
if (!is_subclass_of($sModuleInstallerClass, 'ModuleInstallerAPI')) {
\IssueLog::Error(__METHOD__, null, $aModuleInfo);
throw new CoreException("Wrong installer class: '$sModuleInstallerClass' is not derived from 'ModuleInstallerAPI' - Module: ".$aModuleInfo['label']);
}
@@ -203,7 +192,7 @@ class ModuleFileReader
/**
* @param string $sModuleFilePath
* @param \PhpParser\Node\Stmt\Expression $oExpression
* @param \PhpParser\Node\Expr\Assign $oAssignation
*
* @return array|null
* @throws ModuleFileReaderException

View File

@@ -130,7 +130,6 @@ abstract class ModuleInstallerAPI
if (in_array($sTo, $aNewValues)) {
$sEnumCol = $oAttDef->Get("sql");
$aFields = CMDBSource::QueryToArray("SHOW COLUMNS FROM `$sTableName` WHERE Field = '$sEnumCol'");
$aCurrentValues = [];
if (isset($aFields[0]['Type'])) {
$sColType = $aFields[0]['Type'];
// Note: the parsing should rely on str_getcsv (requires PHP 5.3) to cope with escaped string

View File

@@ -7,7 +7,6 @@ class InvalidParameterException extends Exception
abstract class Parameters
{
public $aData = null;
private ?array $aParamValues = null;
public function __construct()
{
@@ -27,26 +26,24 @@ abstract class Parameters
*/
public function GetParamForConfigArray()
{
if (is_null($this->aParamValues)) {
$aDBParams = $this->Get('database');
$this->aParamValues = [
'mode' => $this->Get('mode'),
'db_server' => $aDBParams['server'],
'db_user' => $aDBParams['user'],
'db_pwd' => $aDBParams['pwd'],
'db_name' => $aDBParams['name'],
'new_db_name' => $aDBParams['name'],
'db_prefix' => $aDBParams['prefix'],
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
'db_tls_ca' => $aDBParams['db_tls_ca'],
'application_path' => $this->Get('url', ''),
'language' => $this->Get('language', ''),
'graphviz_path' => $this->Get('graphviz_path', ''),
'source_dir' => $this->Get('source_dir', ''),
];
}
$aDBParams = $this->Get('database');
$aParamValues = [
'mode' => $this->Get('mode'),
'db_server' => $aDBParams['server'],
'db_user' => $aDBParams['user'],
'db_pwd' => $aDBParams['pwd'],
'db_name' => $aDBParams['name'],
'new_db_name' => $aDBParams['name'],
'db_prefix' => $aDBParams['prefix'],
'db_tls_enabled' => $aDBParams['db_tls_enabled'],
'db_tls_ca' => $aDBParams['db_tls_ca'],
'application_path' => $this->Get('url', ''),
'language' => $this->Get('language', ''),
'graphviz_path' => $this->Get('graphviz_path', ''),
'source_dir' => $this->Get('source_dir', ''),
];
return $this->aParamValues;
return $aParamValues;
}
public function Set($sCode, $value)
@@ -107,9 +104,7 @@ class PHPParameters extends Parameters
{
if ($this->aData == null) {
require_once($sParametersFile);
if (isset($ITOP_PARAMS)) {
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
}
$this->aData = $ITOP_PARAMS; // Defined in the file loaded just above
}
}
}

View File

@@ -126,8 +126,9 @@ class RunTimeEnvironment
* from the given file
* @param $oConfig object The configuration (volatile, not necessarily already on disk)
* @param $bModelOnly boolean Whether or not to allow loading a data model with no corresponding DB
* @return none
*/
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false): void
public function InitDataModel($oConfig, $bModelOnly = true, $bUseCache = false)
{
require_once APPROOT.'/setup/moduleinstallation.class.inc.php';
@@ -347,7 +348,6 @@ class RunTimeEnvironment
//
$oFactory = new ModelFactory($sSourceDirFull);
$aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir);
$oModule = null;
foreach ($aModulesToCompile as $oModule) {
if ($oModule instanceof MFDeltaModule) {
// Just before loading the delta, let's save an image of the datamodel
@@ -357,7 +357,7 @@ class RunTimeEnvironment
$oFactory->LoadModule($oModule);
}
if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) {
if ($oModule instanceof MFDeltaModule) {
// A delta was loaded, let's save a second copy of the datamodel
$oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sTargetEnv.'-with-delta.xml');
} else {
@@ -549,14 +549,9 @@ class RunTimeEnvironment
//
$aAvailableModules = $this->AnalyzeInstallation($oConfig, $this->GetBuildDir());
foreach ($aSelectedModuleCodes as $sModuleId) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (!array_key_exists($sModuleId, $aAvailableModules)) {
continue;
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['available_version'];
@@ -630,7 +625,7 @@ class RunTimeEnvironment
public function GetApplicationVersion(Config $oConfig)
{
try {
$aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig);
$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'));
@@ -668,8 +663,7 @@ class RunTimeEnvironment
$aResult['datamodel_version'] = $aResult['product_version'];
}
$this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
return count($aResult) == 0 ? false : $aResult;
return empty($aResult) ? false : $aResult;
}
public static function MakeDirSafe($sDir)
@@ -857,20 +851,16 @@ class RunTimeEnvironment
/**
* Call the given handler method for all selected modules having an installation handler
* @param array[] $aAvailableModules
* @param string[] $aSelectedModules
* @param string $sHandlerName
* @param string[]|null $aSelectedModules
* @throws CoreException
*/
public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null)
public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName)
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aModule, $sHandlerName, $aArgs);
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
}
}
}
@@ -897,7 +887,6 @@ class RunTimeEnvironment
try {
call_user_func_array($aCallSpec, $aArgs);
} catch (Exception $e) {
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID] ?? "";
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
@@ -914,10 +903,10 @@ class RunTimeEnvironment
/**
* Load data from XML files for the selected modules (structural data and/or sample data)
* @param array[] $aAvailableModules All available modules and their definition
* @param string[] $aSelectedModules List of selected modules
* @param bool $bSampleData Wether or not to load sample data
* @param null|string[] $aSelectedModules List of selected modules
*/
public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null)
public function LoadData($aAvailableModules, $aSelectedModules, $bSampleData)
{
$oDataLoader = new XMLDataLoader();
@@ -930,33 +919,30 @@ class RunTimeEnvironment
$aFiles = [];
$aPreviouslyLoadedFiles = [];
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) {
continue;
}
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (is_null($aSelectedModules) || in_array($sModuleId, $aSelectedModules)) {
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']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
if (($sModuleId != ROOT_MODULE)) {
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (in_array($sModuleId, $aSelectedModules)) {
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']);
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
} else {
// Load only structural data
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
} else {
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
if ($bSampleData) {
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.sample']);
} else {
// Load only structural data
$aFiles = static::MergeWithRelativeDir($aFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);
}
}
}
}
}
// Simulate the load of the previously loaded files, in order to initialize
@@ -965,7 +951,7 @@ class RunTimeEnvironment
foreach ($aPreviouslyLoadedFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)");
if (!file_exists($sFileName)) {
if (empty($sFileName) || !file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}
@@ -977,7 +963,7 @@ class RunTimeEnvironment
foreach ($aFiles as $sFileRelativePath) {
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName");
if (!file_exists($sFileName)) {
if (empty($sFileName) || !file_exists($sFileName)) {
throw(new Exception("File $sFileName does not exist"));
}

View File

@@ -254,23 +254,32 @@ class SetupUtils
if (!utils::IsModeCLI()) {
$sUploadTmpDir = self::GetUploadTmpDir();
// check that the upload directory is indeed writable from PHP
if (!file_exists($sUploadTmpDir)) {
if (empty($sUploadTmpDir)) {
$sUploadTmpDir = '/tmp';
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
CheckResult::WARNING,
"Temporary directory for files upload is not defined (upload_tmp_dir), assuming that $sUploadTmpDir is used."
);
} else {
if (!is_writable($sUploadTmpDir)) {
}
// check that the upload directory is indeed writable from PHP
if (!empty($sUploadTmpDir)) {
if (!file_exists($sUploadTmpDir)) {
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
"Temporary directory for files upload ($sUploadTmpDir) does not exist or cannot be read by PHP."
);
} else {
$aResult[] = new CheckResult(
CheckResult::TRACE,
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
);
if (!is_writable($sUploadTmpDir)) {
$aResult[] = new CheckResult(
CheckResult::ERROR,
"Temporary directory for files upload ($sUploadTmpDir) is not writable."
);
} else {
$aResult[] = new CheckResult(
CheckResult::TRACE,
"Info - Temporary directory for files upload ($sUploadTmpDir) is writable."
);
}
}
}
}
@@ -590,7 +599,7 @@ class SetupUtils
// create and test destination location
//
$sDestDir = dirname($sDBBackupPath);
SetupUtils::builddir($sDestDir);
setuputils::builddir($sDestDir);
if (!is_dir($sDestDir)) {
$aResult[] = new CheckResult(CheckResult::ERROR, "$sDestDir does not exist and could not be created.");
}
@@ -1546,7 +1555,7 @@ JS
return $sHtml;
}
public static function GetConfig(WizardController $oWizard)
public static function GetConfig($oWizard)
{
$oConfig = new Config();
$sSourceDir = $oWizard->GetParameter('source_dir', '');
@@ -1561,7 +1570,7 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = $sRelativeSourceDir;
$oConfig->UpdateFromParams($aParamValues);
$oConfig->UpdateFromParams($aParamValues, null);
return $oConfig;
}
@@ -1593,10 +1602,6 @@ JS
$aDirsToScan[] = $sExtraDir;
}
$oProductionEnv = new RunTimeEnvironment();
$aRemovedExtensionCodes = json_decode($oWizard->GetParameter('removed_extensions'), true) ?? [];
$oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan);
$oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes);
$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan, $bAbortOnMissingDependency, $aModulesToLoad);
foreach ($aAvailableModules as $key => $aModule) {
@@ -1622,7 +1627,7 @@ JS
$aParamValues = $oWizard->GetParamForConfigArray();
$aParamValues['source_dir'] = '';
$oConfig->UpdateFromParams($aParamValues);
$oConfig->UpdateFromParams($aParamValues, null);
$oProductionEnv = new RunTimeEnvironment();
return $oProductionEnv->GetApplicationVersion($oConfig);
@@ -2150,7 +2155,7 @@ class SetupInfo
/**
* Called by the setup process to initializes the list of selected modules. Do not call this method
* from an 'auto_select' rule
* @param hash $aModules
* @param array $aModules
* @return void
*/
public static function SetSelectedModules($aModules)

View File

@@ -71,12 +71,12 @@ class InstallationFileService
return $this->aAfterComputationSelectedExtensions;
}
public function SetItopExtensionsMap(iTopExtensionsMap $oItopExtensionsMap): void
public function SetItopExtensionsMap(ItopExtensionsMap $oItopExtensionsMap): void
{
$this->oItopExtensionsMap = $oItopExtensionsMap;
}
public function GetItopExtensionsMap(): iTopExtensionsMap
public function GetItopExtensionsMap(): ItopExtensionsMap
{
if (is_null($this->oItopExtensionsMap)) {
$this->oItopExtensionsMap = new iTopExtensionsMap($this->sTargetEnvironment);

View File

@@ -16,6 +16,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\WebPage\WebPage;
/**
@@ -401,6 +402,18 @@ abstract class WizardStep
* @return void
*/
abstract public function Display(WebPage $oPage);
/**
* Displays the wizard page for the current class/state
* return UIBlock
* The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
* (this is reserved for the wizard's own parameters)
* @return \Combodo\iTop\Application\UI\Base\UIBlock
* @since 3.0.0
*/
public function DisplayBlock(WebPage $oPage)
{
return new Html($this->Display($oPage));
}
/**
* Processes the page's parameters and (if moving forward) returns the next step/state to be displayed

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
@@ -671,13 +672,9 @@ class WizStepLicense extends WizardStep
private function NeedsGdprConsent()
{
$sMode = $this->oWizard->GetParameter('install_mode');
if ($sMode !== 'install') {
return false;
}
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
return SetupUtils::IsConnectableToITopHub($aModules);
return (($sMode === 'install') && SetupUtils::IsConnectableToITopHub($aModules));
}
/**
@@ -1325,8 +1322,6 @@ class WizStepModulesChoice extends WizardStep
*/
protected iTopExtensionsMap $oExtensionsMap;
private ?array $aSteps = null;
protected PhpExpressionEvaluator $oPhpExpressionEvaluator;
/**
@@ -1335,9 +1330,6 @@ class WizStepModulesChoice extends WizardStep
*/
protected bool $bChoicesFromDatabase;
private array $aAnalyzeInstallationModules;
private ?MissingDependencyException $oMissingDependencyException = null;
public function __construct(WizardController $oWizard, $sCurrentState)
{
parent::__construct($oWizard, $sCurrentState);
@@ -1362,21 +1354,13 @@ class WizStepModulesChoice extends WizardStep
$this->oExtensionsMap->LoadChoicesFromDatabase($this->oConfig);
$this->bChoicesFromDatabase = true;
}
// Sanity check (not stopper, to let developers go further...)
try {
$this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard, true);
} catch (MissingDependencyException $e) {
$this->oMissingDependencyException = $e;
$this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard);
}
}
public function GetTitle(): string
public function GetTitle()
{
$aStepInfo = $this->GetStepInfo();
return $aStepInfo['title'] ?? 'Modules selection';
$sTitle = isset($aStepInfo['title']) ? $aStepInfo['title'] : 'Modules selection';
return $sTitle;
}
public function GetPossibleSteps()
@@ -1431,16 +1415,13 @@ class WizStepModulesChoice extends WizardStep
$sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedChoices[$i], $aModules, '', '', $aExtensions);
}
$sDisplayChoices .= '</ul>';
if (class_exists('CreateITILProfilesInstaller')) {
$this->oWizard->SetParameter('old_addon', true);
}
[$aExtensionsAdded, $aExtensionsRemoved, $aExtensionsNotUninstallable] = $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', json_encode($aExtensionsAdded));
$this->oWizard->SetParameter('removed_extensions', json_encode($aExtensionsRemoved));
$this->oWizard->SetParameter('extensions_removed', json_encode($aExtensionsRemoved));
$this->oWizard->SetParameter('extensions_not_uninstallable', json_encode(array_keys($aExtensionsNotUninstallable)));
return ['class' => 'WizStepSummary', 'state' => ''];
}
@@ -1461,8 +1442,10 @@ class WizStepModulesChoice extends WizardStep
protected function DisplayStep($oPage)
{
// Sanity check (not stopper, to let developers go further...)
if (! is_null($this->oMissingDependencyException)) {
$oPage->warning($this->oMissingDependencyException->getHtmlDesc(), $this->oMissingDependencyException->getMessage());
try {
SetupUtils::AnalyzeInstallation($this->oWizard, true);
} catch (MissingDependencyException $e) {
$oPage->warning($e->getHtmlDesc(), $e->getMessage());
}
$this->bUpgrade = ($this->oWizard->GetParameter('install_mode') != 'install');
@@ -1473,8 +1456,9 @@ class WizStepModulesChoice extends WizardStep
$oPage->add_style(".choice-disabled { color: #999; }");
$oPage->add_style("input.unremovable { accent-color: orangered;}");
$aModules = SetupUtils::AnalyzeInstallation($this->oWizard);
$sManualInstallError = SetupUtils::CheckManualInstallDirEmpty(
$this->aAnalyzeInstallationModules,
$aModules,
$this->oWizard->GetParameter('extensions_dir', 'extensions')
);
if ($sManualInstallError !== '') {
@@ -1500,7 +1484,7 @@ class WizStepModulesChoice extends WizardStep
$oPage->add('</div>');
// Build the default choices
$aDefaults = $this->GetDefaults($aStepInfo, $this->aAnalyzeInstallationModules);
$aDefaults = $this->GetDefaults($aStepInfo, $aModules);
$index = $this->GetStepIndex();
// retrieve the saved selection
@@ -1756,11 +1740,11 @@ EOF
*
* @return string A text representation of what will be installed
*/
protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sParentId = '', $sDisplayChoices = '', &$aSelectedExtensions = null)
public function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sParentId = '', $sDisplayChoices = '', &$aSelectedExtensions = null)
{
if ($sParentId == '') {
// Check once (before recursing) that the hidden modules are selected
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
foreach (SetupUtils::AnalyzeInstallation($this->oWizard) as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && !isset($aModules[$sModuleId])) {
if (($aModule['category'] == 'authentication') || (!$aModule['visible'] && !isset($aModule['auto_select']))) {
$aModules[$sModuleId] = true;
@@ -1769,7 +1753,7 @@ EOF
}
}
}
$aOptions = isset($aInfo['options']) ? $aInfo['options'] : [];
$aOptions = $aInfo['options'] ?? [];
foreach ($aOptions as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
$aModuleInfo = [];
@@ -1784,6 +1768,9 @@ EOF
(isset($aSelectedChoices[$sChoiceId]) && ($aSelectedChoices[$sChoiceId] == $sChoiceId))) {
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if (isset($aChoice['modules'])) {
if (count($aChoice['modules']) === 0) {
throw new Exception('Setup option does not have any module associated');
}
foreach ($aChoice['modules'] as $sModuleId) {
$bSelected = true;
if (isset($aModuleInfo[$sModuleId])) {
@@ -1806,7 +1793,6 @@ EOF
}
}
}
$sChoiceType = isset($aChoice['type']) ? $aChoice['type'] : 'wizard_option';
if ($aSelectedExtensions !== null) {
$aSelectedExtensions[] = $aChoice['extension_code'];
}
@@ -1820,7 +1806,7 @@ EOF
}
}
$aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : [];
$aAlternatives = $aInfo['alternatives'] ?? [];
$sChoiceName = null;
foreach ($aAlternatives as $index => $aChoice) {
$sChoiceId = $sParentId.self::$SEP.$index;
@@ -1850,10 +1836,11 @@ EOF
if ($sParentId == '') {
// Last pass (after all the user's choices are turned into "selected" modules):
// Process 'auto_select' modules for modules that are not already selected
$aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard);
do {
// Loop while new modules are added...
$bModuleAdded = false;
foreach ($this->aAnalyzeInstallationModules as $sModuleId => $aModule) {
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && !array_key_exists($sModuleId, $aModules) && isset($aModule['auto_select'])) {
try {
SetupInfo::SetSelectedModules($aModules);
@@ -1891,72 +1878,117 @@ EOF
protected function GetStepInfo($idx = null)
{
$index = $idx ?? $this->GetStepIndex();
if (is_null($this->aSteps)) {
$this->oWizard->SetParameter('additional_extensions_modules', json_encode([])); // Default value, no additional extensions
if (@file_exists($this->GetSourceFilePath())) {
// Found an "installation.xml" file, let's use this definition for the wizard
$aParams = new XMLParameters($this->GetSourceFilePath());
$this->aSteps = $aParams->Get('steps', []);
if ($index + 1 >= count($this->aSteps)) {
//make sure we also cache next step as well
$aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo();
// Display this step of the wizard only if there is something to display
if (count($aOptions) > 0) {
$this->aSteps[] = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => $aOptions,
];
$this->oWizard->SetParameter('additional_extensions_modules', json_encode($aOptions));
}
}
} else {
$aOptions = $this->oExtensionsMap->GetAllExtensionsOptionInfo();
// No wizard configuration provided, build a standard one with just one big list. All items are mandatory, only works when there are no conflicted modules.
$this->aSteps = [
[
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
'banner' => '/images/icons/icons8-apps-tab.svg',
'options' => $aOptions,
],
];
}
$aStepInfo = null;
if ($idx === null) {
$index = $this->GetStepIndex();
} else {
$index = $idx;
}
return $this->aSteps[$index] ?? null;
$aSteps = [];
$this->oWizard->SetParameter('additional_extensions_modules', json_encode([])); // Default value, no additional extensions
if (@file_exists($this->GetSourceFilePath())) {
// Found an "installation.xml" file, let's use this definition for the wizard
$aParams = new XMLParameters($this->GetSourceFilePath());
$aSteps = $aParams->Get('steps', []);
// Additional step for the "extensions"
$aStepDefinition = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => [],
];
foreach ($this->oExtensionsMap->GetAllExtensionsWithPreviouslyInstalled() as $oExtension) {
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) {
$aStepDefinition['options'][] = [
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory || ($oExtension->sSource === iTopExtension::SOURCE_REMOTE),
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
'uninstallable' => $oExtension->CanBeUninstalled(),
'missing' => $oExtension->bRemovedFromDisk,
];
}
}
// Display this step of the wizard only if there is something to display
if (count($aStepDefinition['options']) !== 0) {
$aSteps[] = $aStepDefinition;
$this->oWizard->SetParameter('additional_extensions_modules', json_encode($aStepDefinition['options']));
}
} else {
// No wizard configuration provided, build a standard one with just one big list. All items are mandatory, only works when there are no conflicted modules.
$aStepDefinition = [
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
'banner' => '/images/icons/icons8-apps-tab.svg',
'options' => [],
];
foreach ($this->oExtensionsMap->GetAllExtensions() as $oExtension) {
if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0)) {
$aStepDefinition['options'][] = [
'extension_code' => $oExtension->sCode,
'title' => $oExtension->sLabel,
'description' => $oExtension->sDescription,
'more_info' => $oExtension->sMoreInfoUrl,
'default' => true, // by default offer to install all modules
'modules' => $oExtension->aModules,
'mandatory' => $oExtension->bMandatory || ($oExtension->sSource !== iTopExtension::SOURCE_REMOTE),
'source_label' => $this->GetExtensionSourceLabel($oExtension->sSource),
];
}
}
$aSteps[] = $aStepDefinition;
}
if (array_key_exists($index, $aSteps)) {
$aStepInfo = $aSteps[$index];
}
return $aStepInfo;
}
protected function GetExtensionSourceLabel($sSource)
{
$sDecorationClass = '';
switch ($sSource) {
case iTopExtension::SOURCE_MANUAL:
$sResult = 'Local extensions folder';
$sDecorationClass = 'fas fa-folder';
break;
case iTopExtension::SOURCE_REMOTE:
$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer';
$sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler';
break;
default:
$sResult = '';
}
if ($sResult == '') {
return '';
}
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']);
//If the extension is missing from disk, it won't exist in the ExtensionsMap, thus returning null
$bCanBeUninstalled = isset($aChoice['uninstallable']) ? $aChoice['uninstallable'] === true || $aChoice['uninstallable'] === 'yes' : $oITopExtension->CanBeUninstalled();
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId);
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || $bUpgradeMode && $oITopExtension->bInstalled && !$bCanBeUninstalled && !$bDisableUninstallCheck;
$bChecked = $bSelected;
$bDisabled = false;
if ($bMissingFromDisk) {
$bDisabled = true;
$bChecked = false;
}
elseif($bMandatory || $bInstalled && !$bCanBeUninstalled){
$bDisabled = true;
$bChecked = true;
}
if($bAllDisabled){
$bDisabled = true;
}
$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'] ?? [];
@@ -1999,17 +2031,17 @@ EOF
$sTooltip = '';
$sUnremovable = '';
if ($aFlags['missing']) {
$sTooltip .= '<div class="setup-extension-tag removed">source removed</div>';
$sTooltip .= '<span class="setup-extension-tag removed">source removed</span>';
}
if ($aFlags['installed']) {
$sTooltip .= '<div class="setup-extension-tag checked installed">installed</div>';
$sTooltip .= '<div class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</div>';
$sTooltip .= '<span class="setup-extension-tag checked installed">installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked tobeuninstalled">to be uninstalled</span>';
} else {
$sTooltip .= '<div class="setup-extension-tag checked tobeinstalled">to be installed</div>';
$sTooltip .= '<div class="setup-extension-tag unchecked notinstalled">not installed</div>';
$sTooltip .= '<span class="setup-extension-tag checked tobeinstalled">to be installed</span>';
$sTooltip .= '<span class="setup-extension-tag unchecked notinstalled">not installed</span>';
}
if (!$aFlags['uninstallable']) {
$sTooltip .= '<div class="setup-extension-tag notuninstallable">cannot be uninstalled</div>';
$sTooltip .= '<span class="setup-extension-tag notuninstallable">cannot be uninstalled</span>';
}
if ($aFlags['disabled'] && !$aFlags['checked'] && !$aFlags['uninstallable'] && !$bDisableUninstallCheck) {
$this->bCanMoveForward = false;//Disable "Next"
@@ -2134,6 +2166,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();
@@ -2202,10 +2246,11 @@ class WizStepSummary extends WizardStep
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be installed</span>');
$aExtensionsAdded = json_decode($this->oWizard->GetParameter('extensions_added'), true);
if (count($aExtensionsAdded) > 0) {
$sExtensionsAdded = '';
if (count($aExtensionsAdded)) {
$sExtensionsAdded = '<ul>';
foreach ($aExtensionsAdded as $sExtensionCode => $sLabel) {
$sExtensionsAdded .= "<li>$sLabel</li>'";
$sExtensionsAdded .= '<li>'.$sLabel.'</li>';
}
$sExtensionsAdded .= '</ul>';
} else {
@@ -2215,16 +2260,17 @@ class WizStepSummary extends WizardStep
$oPage->add('</div>');
$oPage->add('<div class="closed"><span class="title ibo-setup-summary-title">Extensions to be uninstalled</span>');
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('removed_extensions'), true) ?? [];
$aExtensionsRemoved = json_decode($this->oWizard->GetParameter('extensions_removed'), true);
$aExtensionsNotUninstallable = json_decode($this->oWizard->GetParameter('extensions_not_uninstallable'));
$sExtensionsRemoved = '';
if (count($aExtensionsRemoved) > 0) {
$sExtensionsRemoved = '<ul>';
foreach ($aExtensionsRemoved as $sExtensionCode => $sLabel) {
$sForcedUninstall = '';
if (in_array($sExtensionCode, $aExtensionsNotUninstallable)) {
$sExtensionsRemoved .= "<li>$sLabel (forced uninstallation)</li>";
} else {
$sExtensionsRemoved .= "<li>$sLabel</li>";
$sForcedUninstall = ' (forced uninstallation)';
}
$sExtensionsRemoved .= '<li>'.$sLabel.$sForcedUninstall.'</li>';
}
$sExtensionsRemoved .= '</ul>';
} else {
@@ -2286,6 +2332,8 @@ class WizStepSummary extends WizardStep
}
$aSelectedModules = $aInstallParams['selected_modules'];
if (isset($aMiscOptions['generate_config'])) {
$oDoc = new DOMDocument('1.0', 'UTF-8');
$oDoc->preserveWhiteSpace = false;

View File

@@ -230,7 +230,6 @@ class XMLDataLoader
} else {
$iDstObj = (int)($oSubNode);
// Attempt to find the object in the list of loaded objects
/** @var \Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey $oAttDef */
$iExtKey = $this->GetObjectKey($oAttDef->GetTargetClass(), $iDstObj);
if ($iExtKey == 0) {
$iExtKey = -$iDstObj; // Convention: Unresolved keys are stored as negative !
@@ -354,10 +353,8 @@ class XMLDataLoader
foreach ($oObjList as $oTargetObj) {
$bChanged = false;
$sClass = get_class($oTargetObj);
$iExtKey = -1;
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
if (($oAttDef->IsExternalKey()) && ($oTargetObj->Get($sAttCode) < 0)) { // Convention unresolved key = negative
/** @var \Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey $oAttDef */
$sTargetClass = $oAttDef->GetTargetClass();
$iTempKey = $oTargetObj->Get($sAttCode);

View File

@@ -25,7 +25,6 @@ class JsonPage extends WebPage
* This can be useful when feeding response to a third party lib that doesn't understand the structured format.
*/
protected $bOutputDataOnly = false;
protected $bOutputHeaders = true;
/**
* JsonPage constructor.
@@ -83,19 +82,6 @@ class JsonPage extends WebPage
return $this;
}
/**
* @see static::$bOutputHeaders
* @param bool $bFlag
*
* @return $this
*/
public function SetOutputHeaders(bool $bFlag)
{
$this->bOutputHeaders = $bFlag;
return $this;
}
/**
* Output the headers
*
@@ -133,10 +119,7 @@ class JsonPage extends WebPage
public function output()
{
$oKpi = new ExecutionKPI();
if ($this->bOutputHeaders) {
$this->OutputHeaders();
}
$this->OutputHeaders();
$sContent = $this->ComputeContent();
$oKpi->ComputeAndReport(get_class($this).' output');

View File

@@ -178,19 +178,12 @@ class InterfaceDiscovery
continue;
}
$aTmpClassMap = include $sAutoloadFile;
if (! is_array($aTmpClassMap)) {
//can happen when setup compilation broken in the middle
//ex: $sAutoloadFile could be empty and $aTmpClassMap is a int
$aAutoloaderErrors[] = $sAutoloadFile;
continue;
}
/** @noinspection SlowArrayOperationsInLoopInspection we are getting an associative array so the documented workarounds cannot be used */
$aClassMap = array_merge($aClassMap, $aTmpClassMap);
}
if (count($aAutoloaderErrors) > 0) {
IssueLog::Debug(
__METHOD__." cannot load some of the autoloader files: missing or corrupted",
__METHOD__." cannot load some of the autoloader files",
LogChannels::CORE,
['autoloader_errors' => $aAutoloaderErrors]
);

View File

@@ -1,80 +0,0 @@
<?php
namespace Combodo\iTop\Test\UnitTest\HubConnector;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\HubConnector\Controller\HubController;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DOMFormatException;
use JsonPage;
use MFCompiler;
use SetupLog;
use SetupUtils;
use utils;
/**
* @runClassInSeparateProcess
*/
class HubControllerTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
public const AUTHENTICATION_TOKEN = '14b5da9d092f84044187421419a0347e7317bc8cd2b486fdda631be06b959269';
public const AUTHENTICATION_PASSWORD = "tagada-Secret,007";
protected function setUp(): void
{
$this->SkipIfModuleNotPresent('itop-hub-connector');
parent::setUp();
$this->RequireOnceItopFile('env-production/itop-hub-connector/src/Controller/HubController.php');
}
public function testLaunchCompile(): void
{
$this->PrepareCompileAuthent();
$this->CopyProductionModulesIntoHubExtensionDir();
HubController::GetInstance()->SetOutputHeaders(false);
HubController::GetInstance()->LaunchCompile();
$this->CheckReport('{"code":0,"message":"Ok","fields":[]}');
}
public function testLaunchDeploy(): void
{
$this->testLaunchCompile();
HubController::GetInstance()->LaunchDeploy();
$this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}');
$this->AssertPreviousAndCurrentInstallationAreEquivalent();
}
private function CheckReport($sExpected)
{
$oJsonPage = HubController::GetInstance()->GetLastJsonPage();
$this->assertEquals($sExpected, $this->InvokeNonPublicMethod(JsonPage::class, 'ComputeContent', $oJsonPage, []));
//keep line below to avoid: Test code or tested code did not (only) close its own output buffers
$this->InvokeNonPublicMethod(JsonPage::class, 'RenderContent', $oJsonPage, []);
}
private function PrepareCompileAuthent()
{
$sUUID = 'hub_'.uniqid();
$_REQUEST['authent'] = $sUUID;
$sPath = utils::GetDataPath().'hub/compile_authent';
file_put_contents($sPath, $sUUID);
$this->aFileToClean[] = $sPath;
}
private function CopyProductionModulesIntoHubExtensionDir()
{
$sProdModules = APPROOT.'/data/production-modules';
$sExtensionDir = APPROOT.'/data/downloaded-extensions/';
$this->aFileToClean[] = $sExtensionDir;
SetupUtils::rrmdir($sExtensionDir);
@mkdir($sExtensionDir);
SetupUtils::copydir($sExtensionDir, $sProdModules);
}
}

View File

@@ -17,7 +17,6 @@ namespace Combodo\iTop\Test\UnitTest;
use ArchivedObjectException;
use CMDBObject;
use CMDBSource;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\Service\Events\EventService;
use Config;
use Contact;
@@ -35,7 +34,6 @@ use lnkContactToTicket;
use lnkFunctionalCIToTicket;
use MetaModel;
use MissingQueryArgument;
use ModuleInstallationRepository;
use MySQLException;
use MySQLHasGoneAwayException;
use Person;
@@ -1558,31 +1556,4 @@ abstract class ItopDataTestCase extends ItopTestCase
@chmod($sConfigPath, 0440);
@unlink($this->sConfigTmpBackupFile);
}
public function AssertPreviousAndCurrentInstallationAreEquivalent()
{
$aPreviousInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset(1);
$aInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset();
$this->assertEquals($this->GetCanonicalComparableModuleInstallationArray($aPreviousInstallations), $this->GetCanonicalComparableModuleInstallationArray($aInstallations));
}
protected function GetCanonicalComparableModuleInstallationArray($aInstallations): array
{
$aRes = [];
$aIgnoredFields = ['id', 'parent_id', 'installed', 'comment'];
foreach ($aInstallations as $aData) {
$aNewData = [];
foreach ($aData as $sKey => $val) {
if (in_array($sKey, $aIgnoredFields)) {
continue;
}
$aNewData[$sKey] = $val;
}
$sName = $aNewData['name'];
$aRes[$sName] = $aNewData;
}
asort($aRes);
return $aRes;
}
}

View File

@@ -698,17 +698,4 @@ abstract class ItopTestCase extends KernelTestCase
return $this->CallUrl($sUrl, $aPostFields, $aCurlOptions, $bXDebugEnabled);
}
/**
* Return a temporary file path. that will be cleaned up by tearDown()
*
* @return string: temporary file path: file prefix include phpunit test method name
*/
public function GetTemporaryFilePath(): string
{
$sPrefix = $this->getName(false);
$sPath = tempnam(sys_get_temp_dir(), $sPrefix);
$this->aFileToClean[] = $sPath;
return $sPath;
}
}

View File

@@ -8,13 +8,11 @@
namespace Combodo\iTop\Test\UnitTest\Service;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DOMFormatException;
use MFCoreModule;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RunTimeEnvironment;
use SetupLog;
use SetupUtils;
use utils;
@@ -66,13 +64,7 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment
}
}
try {
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
} catch (DOMFormatException $e) {
$sFileName = $sSourceEnv.'.delta.xml';
SetupLog::Error(__METHOD__, null, [$sFileName => @file_get_contents(APPROOT.'data/'.$sFileName)]);
throw $e;
}
parent::CompileFrom($sSourceEnv, $bUseSymLinks);
}
public function IsUpToDate()

View File

@@ -0,0 +1,50 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class ModuleDiscoveryTest extends ItopTestCase
{
public function GetModuleNameProvider()
{
return [
'nominal' => [
'sModuleId' => 'a/1.2.3',
'name' => 'a',
'version' => '1.2.3',
],
'develop' => [
'sModuleId' => 'a/1.2.3-dev',
'name' => 'a',
'version' => '1.2.3-dev',
],
'missing version => 1.0.0' => [
'sModuleId' => 'a/',
'name' => 'a',
'version' => '1.0.0',
],
'missing everything except name' => [
'sModuleId' => 'a',
'name' => 'a',
'version' => '1.0.0',
],
];
}
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
/**
* @dataProvider GetModuleNameProvider
*/
public function testGetModuleName($sModuleId, $expectedName, $expectedVersion)
{
$this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId));
}
}

View File

@@ -4,7 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Setup;
use AnalyzeInstallation;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use ModuleInstallationRepository;
use ModuleInstallationService;
class AnalyzeInstallationTest extends ItopTestCase
{
@@ -12,7 +12,7 @@ class AnalyzeInstallationTest extends ItopTestCase
{
parent::setUp();
$this->RequireOnceItopFile('setup/AnalyzeInstallation.php');
$this->RequireOnceItopFile('setup/ModuleInstallationRepository.php');
$this->RequireOnceItopFile('setup/ModuleInstallationService.php');
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
$this->RequireOnceItopFile('setup/runtimeenv.class.inc.php');
}
@@ -151,7 +151,7 @@ class AnalyzeInstallationTest extends ItopTestCase
$this->SetNonPublicProperty(AnalyzeInstallation::GetInstance(), "aAvailableModules", $aAvailableModules);
//$aModules = json_decode(file_get_contents(__DIR__.'/ressources/priv_modules2.json'), true);
$this->SetNonPublicProperty(ModuleInstallationRepository::GetInstance(), "aSelectInstall", $aInstalledModules);
$this->SetNonPublicProperty(ModuleInstallationService::GetInstance(), "aSelectInstall", $aInstalledModules);
$oConfig = $this->createMock(\Config::class);

View File

@@ -2,9 +2,7 @@
namespace Combodo\iTop\Test\UnitTest\Setup;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use iTopExtension;
use MissingDependencyException;
use ModuleDiscovery;
@@ -17,12 +15,6 @@ class ModuleDiscoveryTest extends ItopTestCase
$this->RequireOnceItopFile('setup/modulediscovery.class.inc.php');
}
protected function tearDown(): void
{
parent::tearDown();
ModuleDiscovery::DeclareRemovedExtensions([]);
}
public function testOrderModulesByDependencies_RealExample()
{
$aModules = json_decode(file_get_contents(__DIR__.'/ressources/reallife_discovered_modules.json'), true);
@@ -85,126 +77,4 @@ TXT;
ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
}
public function testOrderModulesByDependencies_FailWhenChoosenModuleDependsOnRemovedExtensionModule()
{
$aChoices = ['id1', 'id2'];
$sModuleFilePath = $this->GetTemporaryFilePath();
$sModuleFilePath2 = $this->GetTemporaryFilePath();
$aModules = [
"id1/1" => [
'dependencies' => [ 'id2/2'],
'label' => 'label1',
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
],
"id2/2" => [
'dependencies' => [],
'label' => 'label2',
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath2,
],
];
$oExtension = $this->GivenExtensionWithModule('id2', '2', $sModuleFilePath2);
ModuleDiscovery::DeclareRemovedExtensions([$oExtension]);
$sExpectedMessage = <<<TXT
The following modules have unmet dependencies:
label1 (id: id1/1) depends on: ❌ id2/2
TXT;
$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessage($sExpectedMessage);
ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices);
}
public function GetModuleNameProvider()
{
return [
'nominal' => [
'sModuleId' => 'a/1.2.3',
'name' => 'a',
'version' => '1.2.3',
],
'develop' => [
'sModuleId' => 'a/1.2.3-dev',
'name' => 'a',
'version' => '1.2.3-dev',
],
'missing version => 1.0.0' => [
'sModuleId' => 'a/',
'name' => 'a',
'version' => '1.0.0',
],
'missing everything except name' => [
'sModuleId' => 'a',
'name' => 'a',
'version' => '1.0.0',
],
];
}
/**
* @dataProvider GetModuleNameProvider
*/
public function testGetModuleName($sModuleId, $expectedName, $expectedVersion)
{
$this->assertEquals([$expectedName, $expectedVersion], ModuleDiscovery::GetModuleName($sModuleId));
}
public function testIsModuleInExtensionList_NoRemovedExtension()
{
$this->assertFalse($this->InvokeNonPublicStaticMethod(ModuleDiscovery::class, "IsModuleInExtensionList", [[], 'module_name', '123', []]));
}
public function testIsModuleInExtensionList_ModuleWithAnotherVersionIncludedInRemoveExtension()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '123', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_AnotherModuleWithSameVersionIncludedInRemoveExtension()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'another_module_name', '456', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_SameExtensionComingFromAnotherLocation()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$sModuleFilePath2 = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath2)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '456', $sModuleFilePath, false);
}
public function testIsModuleInExtensionList_ModuleShouldBeExcluded()
{
$sModuleFilePath = $this->GetTemporaryFilePath();
$aExtensionList = [$this->GivenExtensionWithModule('module_name', '456', $sModuleFilePath)];
$this->AssertModuleIsPartOfRemovedExtension($aExtensionList, 'module_name', '456', $sModuleFilePath, true);
}
public function AssertModuleIsPartOfRemovedExtension($aExtensionList, $sModuleName, $sModuleVersion, $sModuleFilePath, $bExpected)
{
$aCurrentModuleInfo = [
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
];
$this->assertEquals(
$bExpected,
$this->InvokeNonPublicStaticMethod(ModuleDiscovery::class, "IsModuleInExtensionList", [$aExtensionList, $sModuleName, $sModuleVersion, $aCurrentModuleInfo])
);
}
private function GivenExtensionWithModule(string $sModuleName, string $sVersion, bool|string $sModuleFilePath): iTopExtension
{
$oExt = new iTopExtension();
$oExt->aModuleVersion[$sModuleName] = $sVersion;
$oExt->aModuleInfo[$sModuleName] = [
ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath,
];
return $oExt;
}
}

View File

@@ -5,7 +5,6 @@ class WizStepModulesChoiceFake extends WizStepModulesChoice
public function __construct(WizardController $oWizard, $sCurrentState)
{
$this->oWizard = $oWizard;
$this->sCurrentState = $sCurrentState;
}
public function setExtensionMap(iTopExtensionsMap $oMap)

View File

@@ -3,16 +3,13 @@
namespace Combodo\iTop\Test\UnitTest\Integration;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use iTopExtensionsMap;
use ItopExtensionsMap;
use iTopExtensionsMapFake;
use ModuleDiscovery;
use WizardController;
use WizStepModulesChoiceFake;
use XMLParameters;
class WizStepModulesChoiceTest extends ItopTestCase
{
private WizStepModulesChoiceFake $oStep;
protected function setUp(): void
{
parent::setUp();
@@ -20,7 +17,7 @@ class WizStepModulesChoiceTest extends ItopTestCase
require_once __DIR__.'/iTopExtensionsMapFake.php';
require_once __DIR__.'/WizStepModulesChoiceFake.php';
$this->oStep = new WizStepModulesChoiceFake(new WizardController('', ''), '');
$this->oStep = new \WizStepModulesChoiceFake(new WizardController('', ''), '');
ModuleDiscovery::ResetCache();
}
@@ -67,60 +64,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
'checked' => true,
],
],
'A missing extension should be disabled and unchecked' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => false,
'missing' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'A missing extension should always be disabled and unchecked, even when mandatory' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'missing' => true,
'uninstallable' => true,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => true,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
'aExtensionsOnDiskOrDb' => [
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
'mandatory' => true,
'missing' => true,
'uninstallable' => false,
],
'bCurrentSelected' => false,
'aExpectedFlags' => [
'uninstallable' => false,
'missing' => true,
'installed' => true,
'disabled' => true,
'checked' => false,
],
],
'An installed but not selected extension should not be checked and be enabled' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
@@ -407,156 +350,98 @@ class WizStepModulesChoiceTest extends ItopTestCase
$this->assertEquals($aExpectedRemovedList, $aRemovedList);
}
public function testGetStepInfo_PackageWithoutInstallationXML()
public function ProviderGetSelectedModules()
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithoutXmlInstallation($aExtensionsOnDiskOrDb);
$expected = [
'title' => 'Modules Selection',
'description' => '<h2>Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.</h2>',
'banner' => '/images/icons/icons8-apps-tab.svg',
'options' => $aExtensionsOnDiskOrDb,
return [
'No extension selected' => [
'aSelected' => [],
'aExpectedModules' => [],
'aExpectedExtensions' => [],
],
'One extension selected' => [
'aSelected' => ['_0' => '_0'],
'aExpectedModules' => ['combodo-sample-module' => true],
'aExpectedExtensions' => ['combodo-sample'],
],
];
$this->CallAndCheckTwice($oWizStepModulesChoice, null, $expected);
$this->CallAndCheckTwice($oWizStepModulesChoice, 1, null);
}
private function GivenWizStepModulesChoiceWithoutXmlInstallation(array $aExtensionsOnDiskOrDb): WizStepModulesChoiceFake
{
$oExtensionsMap = $this->createMock(iTopExtensionsMap::class);
$oExtensionsMap->expects($this->once())
->method('GetAllExtensionsOptionInfo')
->willReturn($aExtensionsOnDiskOrDb);
$oWizard = new WizardController('', '');
$oWizStepModulesChoice = new WizStepModulesChoiceFake($oWizard, '');
$oWizStepModulesChoice->setExtensionMap($oExtensionsMap);
return $oWizStepModulesChoice;
}
public static function PackageWithInstallationXMLProvider()
{
require_once __DIR__.'/../../../../approot.inc.php';
require_once APPROOT.'setup/parameters.class.inc.php';
$aUsecases = [];
$aUsecases["[no step] with extensions"] = [
'iGetStepInfoIdxArg' => null,
'expected' => self::GetStep(0),
];
for ($i = 0; $i < 4; $i++) {
$aUsecases["[step $i] with extensions"] = [
'iGetStepInfoIdxArg' => $i,
'expected' => self::GetStep($i),
];
}
$aUsecases["[step 6] with extensions => NO STEP ANYMORE"] = [
'iGetStepInfoIdxArg' => 6,
'expected' => null,
'iGetAllExtensionsOptionInfoCallCount' => 1,
];
return $aUsecases;
}
/**
* @dataProvider PackageWithInstallationXMLProvider
* @dataProvider ProviderGetSelectedModules
*/
public function testGetStepInfo_PackageWithInstallationXMLWithExtensions($iGetStepInfoIdxArg, $expected, $iGetAllExtensionsOptionInfoCallCount = 0)
public function testGetSelectedModules($aSelectedExtensions, $aExpectedModules, $aExpectedExtensions)
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, $iGetAllExtensionsOptionInfoCallCount);
$this->CallAndCheckTwice($oWizStepModulesChoice, $iGetStepInfoIdxArg, $expected);
}
public function testGetStepInfo_PackageWithInstallationXML_AfterLastStepWithExtensions()
{
$expected = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => self::GivenExtensionsOnDisk(),
];
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, $expected);
}
public function testGetStepInfo_PackageWithInstallationXMLAfterLastStepWithoutExtensions()
{
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation([], 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, null);
}
public function testGetStepInfo_PackageWithInstallationXML_MakeSureNextStepIsAlsoCached()
{
$aExtensionsOnDiskOrDb = self::GivenExtensionsOnDisk();
$oWizStepModulesChoice = $this->GivenWizStepModulesChoiceWithXmlInstallation($aExtensionsOnDiskOrDb, 1);
$this->CallAndCheckTwice($oWizStepModulesChoice, 4, self::GetStep(4));
$expected = [
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions or remove installed ones.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => $aExtensionsOnDiskOrDb,
];
$this->CallAndCheckTwice($oWizStepModulesChoice, 5, $expected);
}
private static function GivenExtensionsOnDisk(): array
{
return [
'itop-ext-added1' => [
'installed' => false,
],
'itop-ext-added2' => [
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => false,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData, ));
//GetSelectedModules
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [
'combodo-sample-module',
],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
],
];
$aModules = [];
$aExtensions = [];
$this->oStep->GetSelectedModules($aStepInfo, $aSelectedExtensions, $aModules, '', '', $aExtensions);
$this->assertEquals($aExpectedModules, $aModules);
$this->assertEquals($aExpectedExtensions, $aExtensions);
}
private function GivenWizStepModulesChoiceWithXmlInstallation(array $aExtensionsOnDiskOrDb, $iGetAllExtensionsOptionInfoCallCount): WizStepModulesChoiceFake
public function testGetSelectedModulesShouldThrowAnExceptionWhenAnySelectedExtensionDoesNotHaveAnyAssociatedModules()
{
$oExtensionsMap = $this->createMock(iTopExtensionsMap::class);
$oExtensionsMap->expects($this->exactly($iGetAllExtensionsOptionInfoCallCount))
->method('GetAllExtensionsOptionInfo')
->willReturn($aExtensionsOnDiskOrDb);
$aExtensionsMapData = [
'combodo-sample' => [
'installed' => false,
],
];
$this->oStep->setExtensionMap(iTopExtensionsMapFake::createFromArray($aExtensionsMapData, ));
$oWizard = new WizardController('', '');
//needed to find installation.xml
$oWizard->SetParameter('source_dir', __DIR__.'/ressources');
$oWizStepModulesChoice = new WizStepModulesChoiceFake($oWizard, '');
$oWizStepModulesChoice->setExtensionMap($oExtensionsMap);
//GetSelectedModules
$aStepInfo = [
'title' => 'Extensions',
'description' => '',
'banner' => '',
'options' => [
[
'extension_code' => 'combodo-sample',
'title' => 'Sample extension',
'description' => '',
'more_info' => '',
'default' => true,
'modules' => [],
'mandatory' => false,
'source_label' => '',
'uninstallable' => true,
'missing' => false,
],
],
];
return $oWizStepModulesChoice;
$aModules = [];
$aExtensions = [];
$this->expectException('Exception');
$this->oStep->GetSelectedModules($aStepInfo, ['_0' => '_0'], $aModules, '', '', $aExtensions);
}
private function CallAndCheckTwice($oStep, $iGetStepInfoIdxArg, $expected)
{
$aRes = $this->InvokeNonPublicMethod(WizStepModulesChoiceFake::class, 'GetStepInfo', $oStep, [$iGetStepInfoIdxArg]);
$this->assertEquals($expected, $aRes, "step:".$iGetStepInfoIdxArg);
$aRes = $this->InvokeNonPublicMethod(WizStepModulesChoiceFake::class, 'GetStepInfo', $oStep, [$iGetStepInfoIdxArg]);
$this->assertEquals($expected, $aRes, "(2nd call) step:".$iGetStepInfoIdxArg);
}
private static function GetStep($index)
{
$aParams = new XMLParameters(__DIR__.'/ressources/installation.xml');
$aSteps = $aParams->Get('steps', []);
return $aSteps[$index] ?? null;
}
}

View File

@@ -3,12 +3,10 @@
namespace Combodo\iTop\Test\UnitTest\Setup\FeatureRemoval;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use Combodo\iTop\Test\UnitTest\Service\UnitTestRunTimeEnvironment;
use MetaModel;
use Exception;
class SetupAuditTest extends ItopCustomDatamodelTestCase
{
@@ -40,7 +38,6 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
parent::setUp();
$this->RequireOnceItopFile('/setup/feature_removal/SetupAudit.php');
$this->RequireOnceItopFile('/setup/feature_removal/InplaceSetupAudit.php');
$this->RequireOnceItopFile('/setup/feature_removal/DryRemovalRuntimeEnvironment.php');
}
@@ -55,7 +52,7 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
$oDryRemovalRuntimeEnvt->Prepare($this->GetTestEnvironment(), ['nominal_ext1', 'finalclass_ext2']);
$oDryRemovalRuntimeEnvt->CompileFrom($this->GetTestEnvironment());
$oSetupAudit = new SetupAudit(MetaModel::GetEnvironment(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$expected = [
"Feature1Module1MyClass",
@@ -70,25 +67,13 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues());
}
public function testGetRemovedClassesFromSetupWizard()
{
$sEnv = MetaModel::GetEnvironment();
$aClassesBeforeRemoval = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnv);
$aClassesBeforeRemoval[] = "GabuZomeu";
$oSetupAudit = new InplaceSetupAudit($aClassesBeforeRemoval, $sEnv);
$oSetupAudit->ComputeClasses();
$this->assertEquals(["GabuZomeu"], $oSetupAudit->GetRemovedClasses());
}
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(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
@@ -114,7 +99,7 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
$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(), DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$oSetupAudit = new SetupAudit(\MetaModel::GetEnvironment());
$aRemovedClasses = [
"Feature1Module1MyClass",
"FinalClassFeature1Module1MyClass",
@@ -126,9 +111,8 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
//avoid setup dry computation
$this->SetNonPublicProperty($oSetupAudit, 'aRemovedClasses', $aRemovedClasses);
$expected = [
"FinalClassFeature1Module1MyFinalClassFromLocation" => 1,
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetIssues(true));
$this->expectException(Exception::class);
$this->expectExceptionMessage('FinalClassFeature1Module1MyFinalClassFromLocation');
$oSetupAudit->GetIssues(true);
}
}

View File

@@ -1,69 +0,0 @@
<?php
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class iTopExtensionTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('/setup/unattended-install/InstallationFileService.php');
ModuleDiscovery::ResetCache();
}
public function testCanBeUninstalledDefaultValueIsTrue()
{
$oExtension = new iTopExtension();
$this->assertTrue($oExtension->CanBeUninstalled(), 'An extension should be uninstallable by default.');
}
public function testCanBeUninstalledReturnTrueWhenAllModulesCanBeUninstalled()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-yes1'] = [
'uninstallable' => 'yes',
];
$oExtension->aModuleInfo['combodo-test-yes2'] = [
'uninstallable' => 'yes',
];
$this->assertTrue($oExtension->CanBeUninstalled(), 'An extension should be considered uninstallable if all of its modules are uninstallable.');
}
public function testCanBeUninstalledReturnFalseWhenAtLeastOneModuleCannotBeUninstalled()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-yes'] = [
'uninstallable' => 'yes',
];
$oExtension->aModuleInfo['combodo-test-no'] = [
'uninstallable' => 'no',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'An extension should be considered non-uninstallable if at least one of its modules is not uninstallable.');
}
public function testCanBeUninstalledAnyValueDifferentThanYesIsConsideredFalse()
{
$oExtension = new iTopExtension();
$oExtension->aModuleInfo['combodo-test-maybe'] = [
'uninstallable' => 'maybe',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'Any value in the uninstallable flag different than yes should be considered false.');
}
public function testCanBeUninstalledExtensionValueOverwriteModulesValue()
{
$oExtension = new iTopExtension();
$oExtension->bCanBeUninstalled = true;
$oExtension->aModuleInfo['combodo-test-no'] = [
'uninstallable' => 'no',
];
$this->assertTrue($oExtension->CanBeUninstalled(), 'The uninstallable flag provided in the extension should prevail over those defined in the modules.');
$oExtension = new iTopExtension();
$oExtension->bCanBeUninstalled = false;
$oExtension->aModuleInfo['combodo-test-yes'] = [
'uninstallable' => 'yes',
];
$this->assertFalse($oExtension->CanBeUninstalled(), 'The uninstallable flag provided in the extension should prevail over those defined in the modules.');
}
}

View File

@@ -1,216 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<installation>
<steps type="array">
<step>
<title>Configuration Management options</title>
<description><![CDATA[<h2>The options below allow you to configure the type of elements that are to be managed inside iTop.</h2>]]></description>
<banner>/images/modules.png</banner>
<options type="array">
<choice>
<extension_code>itop-config-mgmt-core</extension_code>
<title>Configuration Management Core</title>
<description>All the base objects that are mandatory in the iTop CMDB: Organizations, Locations, Teams, Persons, etc.</description>
<modules type="array">
<module>itop-config-mgmt</module>
<module>itop-attachments</module>
<module>itop-profiles-itil</module>
<module>itop-welcome-itil</module>
<module>itop-tickets</module>
<module>itop-files-information</module>
<module>combodo-db-tools</module>
<module>itop-core-update</module>
<module>itop-hub-connector</module>
<module>itop-oauth-client</module>
</modules>
<mandatory>true</mandatory>
</choice>
<choice>
<extension_code>itop-config-mgmt-datacenter</extension_code>
<title>Data Center Devices</title>
<description>Manage Data Center devices such as Racks, Enclosures, PDUs, etc.</description>
<modules type="array">
<module>itop-datacenter-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-end-user</extension_code>
<title>End-User Devices</title>
<description>Manage devices related to end-users: PCs, Phones, Tablets, etc.</description>
<modules type="array">
<module>itop-endusers-devices</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-storage</extension_code>
<title>Storage Devices</title>
<description>Manage storage devices such as NAS, SAN Switches, Tape Libraries and Tapes, etc.</description>
<modules type="array">
<module>itop-storage-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-config-mgmt-virtualization</extension_code>
<title>Virtualization</title>
<description>Manage Hypervisors, Virtual Machines and Farms.</description>
<modules type="array">
<module>itop-virtualization-mgmt</module>
</modules>
<default>true</default>
</choice>
</options>
</step>
<step>
<title>Service Management options</title>
<description><![CDATA[<h2>Select the choice that best describes the relationships between the services and the IT infrastructure in your IT environment.</h2>]]></description>
<banner>./wizard-icons/service.png</banner>
<alternatives type="array">
<choice>
<extension_code>itop-service-mgmt-enterprise</extension_code>
<title>Service Management for Enterprises</title>
<description>Select this option if the IT delivers services based on a shared infrastructure. For example if different organizations within your company subscribe to services (like Mail and Print services) delivered by a single shared backend.</description>
<modules type="array">
<module>itop-service-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-service-mgmt-service-provider</extension_code>
<title>Service Management for Service Providers</title>
<description>Select this option if the IT manages the infrastructure of independent customers. This is the most flexible model, since the services can be delivered with a mix of shared and customer specific infrastructure devices.</description>
<modules type="array">
<module>itop-service-mgmt-provider</module>
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Tickets Management options</title>
<description><![CDATA[<h2>Select the type of tickets you want to use in order to respond to user requests and incidents.</h2>]]></description>
<banner>./itop-incident-mgmt-itil/images/incident-escalated.svg</banner>
<alternatives type="array">
<choice>
<extension_code>itop-ticket-mgmt-simple-ticket</extension_code>
<title>Simple Ticket Management</title>
<description>Select this option to use one single type of tickets for all kind of requests.</description>
<modules type="array">
<module>itop-request-mgmt</module>
</modules>
<default>true</default>
<sub_options>
<options type="array">
<choice>
<extension_code>itop-ticket-mgmt-simple-ticket-enhanced-portal</extension_code>
<title>Customer Portal</title>
<description><![CDATA[Modern & responsive portal for the end-users]]></description>
<modules type="array">
<module>itop-portal</module>
<module>itop-portal-base</module>
</modules>
<default>true</default>
</choice>
</options>
</sub_options>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil</extension_code>
<title>ITIL Compliant Tickets Management</title>
<description>Select this option to have different types of ticket for managing user requests and incidents. Each type of ticket has a specific life cycle and specific fields</description>
<sub_options>
<options type="array">
<choice>
<extension_code>itop-ticket-mgmt-itil-user-request</extension_code>
<title>User Request Management</title>
<description>Manage User Request tickets in iTop</description>
<modules type="array">
<module>itop-request-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil-incident</extension_code>
<title>Incident Management</title>
<description>Manage Incidents tickets in iTop</description>
<modules type="array">
<module>itop-incident-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-itil-enhanced-portal</extension_code>
<title>Customer Portal</title>
<description><![CDATA[Modern & responsive portal for the end-users]]></description>
<modules type="array">
<module>itop-portal</module>
<module>itop-portal-base</module>
</modules>
<default>true</default>
</choice>
</options>
</sub_options>
</choice>
<choice>
<extension_code>itop-ticket-mgmt-none</extension_code>
<title>No Tickets Management</title>
<description>Don't manage incidents or user requests in iTop</description>
<modules type="array">
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Change Management options</title>
<description><![CDATA[<h2>Select the type of tickets you want to use in order to manage changes to the IT infrastructure.</h2>]]></description>
<banner>./itop-change-mgmt/images/change.svg</banner>
<alternatives type="array">
<choice>
<extension_code>itop-change-mgmt-simple</extension_code>
<title>Simple Change Management</title>
<description>Select this option to use one type of ticket for all kind of changes.</description>
<modules type="array">
<module>itop-change-mgmt</module>
</modules>
<default>true</default>
</choice>
<choice>
<extension_code>itop-change-mgmt-itil</extension_code>
<title>ITIL Change Management</title>
<description>Select this option to use Normal/Routine/Emergency change tickets.</description>
<modules type="array">
<module>itop-change-mgmt-itil</module>
</modules>
</choice>
<choice>
<extension_code>itop-change-mgmt-none</extension_code>
<title>No Change Management</title>
<description>Don't manage changes in iTop</description>
<modules type="array">
</modules>
</choice>
</alternatives>
</step>
<step>
<title>Additional ITIL tickets</title>
<description><![CDATA[<h2>Pick from the list below the additional ITIL processes that are to be implemented in iTop.</h2>]]></description>
<banner>./itop-knownerror-mgmt/images/known-error.svg</banner>
<options type="array">
<choice>
<extension_code>itop-kown-error-mgmt</extension_code>
<title>Known Errors Management</title>
<description>Select this option to track "Known Errors" and FAQs in iTop.</description>
<modules type="array">
<module>itop-knownerror-mgmt</module>
</modules>
</choice>
<choice>
<extension_code>itop-problem-mgmt</extension_code>
<title>Problem Management</title>
<description>Select this option track "Problems" in iTop.</description>
<modules type="array">
<module>itop-problem-mgmt</module>
</modules>
</choice>
</options>
</step>
</steps>
</installation>