N°4789 - fix broken setup + tests

This commit is contained in:
odain
2025-08-20 16:26:03 +02:00
parent 1bc14f97e1
commit 07d7995a51
8 changed files with 1132 additions and 1032 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -117,7 +117,7 @@ class ModuleDiscovery
if (!array_key_exists($sArgName, $aArgs))
{
throw new Exception("Module '$sId': missing argument '$sArgName'");
}
}
}
$aArgs['root_dir'] = dirname($sFilePath);
@@ -220,7 +220,7 @@ class ModuleDiscovery
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
* @return array
* @throws \MissingDependencyException
*/
*/
public static function OrderModulesByDependencies($aModules, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
// Order the modules to take into account their inter-dependencies
@@ -351,19 +351,19 @@ class ModuleDiscovery
if (version_compare($sCurrentVersion, $sExpectedVersion, $sOperator))
{
$aReplacements[$sModuleId] = '(true)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
// a function call that results in a runtime fatal error
}
else
{
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
// a function call that results in a runtime fatal error
}
}
else
{
// module is not present
$aReplacements[$sModuleId] = '(false)'; // Add parentheses to protect against invalid condition causing
// a function call that results in a runtime fatal error
// a function call that results in a runtime fatal error
}
}
}
@@ -387,10 +387,10 @@ class ModuleDiscovery
else
{
$sBooleanExpr = str_replace(array_keys($aReplacements), array_values($aReplacements), $sDepString);
$bOk = ModuleDiscoveryService::GetInstance()->ComputeDependencyExpression($sBooleanExpr);
if ($bOk == false)
{
SetupLog::Warning("Eval of '$sBooleanExpr' returned false");
try{
$bResult = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpr);
} catch(ModuleDiscoveryServiceException $e){
//logged already
echo "Failed to parse the boolean Expression = '$sBooleanExpr'<br/>";
}
}
@@ -498,40 +498,12 @@ class ModuleDiscovery
else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches))
{
self::SetModulePath($sRelDir);
try
{
$sModuleFileContents = file_get_contents($sDirectory.'/'.$sFile);
$sModuleFileContents = str_replace(array('<?php', '?>'), '', $sModuleFileContents);
$sModuleFileContents = str_replace('__FILE__', "'".addslashes($sDirectory.'/'.$sFile)."'", $sModuleFileContents);
preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches);
//print_r($aMatches);
$idx = 0;
foreach($aMatches[1] as $sClassName)
{
if (class_exists($sClassName))
{
// rename the class inside the code to prevent a "duplicate class" declaration
// and change its parent class as well so that nobody will find it and try to execute it
$sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents);
}
$idx++;
}
$sModuleFilePath = $sDirectory.'/'.$sFile;
$aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath);
SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo);
//echo "<p>Done.</p>\n";
}
catch(ParseError $e)
{
// PHP 7
SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage()." at line ".$e->getLine());
}
catch(Exception $e)
{
// Continue...
SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage());
$sModuleFilePath = $sDirectory.'/'.$sFile;
try {
$aModuleInfo = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sDirectory.'/'.$sFile);
SetupWebPage::AddModule($sModuleFilePath, $aModuleInfo[1], $aModuleInfo[2]);
} catch(ModuleDiscoveryServiceException $e){
continue;
}
}
}

View File

@@ -2,6 +2,7 @@
class ModuleDiscoveryService {
private static ModuleDiscoveryService $oInstance;
private static int $iDummyClassIndex = 0;
protected function __construct() {
}
@@ -23,13 +24,14 @@ class ModuleDiscoveryService {
* Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles
* @param string $sModuleFile
* @return array
* @throws ModuleDiscoveryServiceException
*/
public function ReadModuleFileConfiguration(string $sModuleFilePath) : array
{
static $iDummyClassIndex = 0;
$aModuleInfo = []; // will be filled by the "eval" line below...
$aModuleInfo = array(); // will be filled by the "eval" line below...
try
{
$aMatches = array();
$sModuleFileContents = file_get_contents($sModuleFilePath);
$sModuleFileContents = str_replace(array('<?php', '?>'), '', $sModuleFileContents);
$sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFilePath)."'", $sModuleFileContents);
@@ -40,47 +42,73 @@ class ModuleDiscoveryService {
{
if (class_exists($sClassName))
{
// rename the class inside the code to prevent a "duplicate class" declaration
// rename any class declaration inside the code to prevent a "duplicate class" declaration
// and change its parent class as well so that nobody will find it and try to execute it
$sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents);
// Note: don't use the same naming scheme as ModuleDiscovery otherwise you 'll have the duplicate class error again !!
$sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_Ext_'.(ModuleDiscoveryService::$iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents);
}
$idx++;
}
// Replace the main function call by an assignment to a variable, as an array...
$sModuleFileContents = str_replace(array('SetupWebPage::AddModule', 'ModuleDiscovery::AddModule'), '$aModuleInfo = array', $sModuleFileContents);
eval($sModuleFileContents); // Assigns $aModuleInfo
if (count($aModuleInfo) === 0)
{
SetupLog::Warning("Eval of $sModuleFilePath did not return the expected information...");
throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath did not return the expected information...");
}
//echo "<p>Done.</p>\n";
}
catch(ModuleDiscoveryServiceException $e)
{
// Continue...
throw $e;
}
catch(ParseError $e)
{
// PHP 7
SetupLog::Warning("Eval of $sModuleFilePath caused a parse exception: ".$e->getMessage()." at line ".$e->getLine());
// Continue...
throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused a parse error: ".$e->getMessage()." at line ".$e->getLine());
}
catch(Exception $e)
{
// Continue...
SetupLog::Warning("Eval of $sModuleFilePath caused an exception: ".$e->getMessage());
throw new ModuleDiscoveryServiceException("Eval of $sModuleFilePath caused an exception: ".$e->getMessage(), 0, $e);
}
return $aModuleInfo;
}
public function ComputeDependencyExpression(string $sBooleanExpr) : bool
/**
* @param string $sBooleanExpr
*
* @return bool
* @throws ModuleDiscoveryServiceException
*/
public function ComputeBooleanExpression(string $sBooleanExpr) : bool
{
return @eval('$bResult = '.$sBooleanExpr.'; return $bResult;');
$bResult = false;
try{
@eval('$bResult = '.$sBooleanExpr.';');
} catch (Throwable $t) {
throw new ModuleDiscoveryServiceException("Eval of '$sBooleanExpr' caused an error: ".$t->getMessage());
}
return $bResult;
}
}
public function ComputeAutoSelectExpression(string $sBooleanExpr) : bool
class ModuleDiscoveryServiceException extends Exception
{
/**
* ModuleDiscoveryServiceException constructor.
*
* @param string $sMessage
* @param int $iHttpCode
* @param Exception|null $oPrevious
*/
public function __construct($sMessage, $iHttpCode = 0, Exception $oPrevious = null)
{
return eval('$bSelected = ('.$sBooleanExpr.'); return $bSelected');
$e = new \Exception("");
SetupLog::Warning($sMessage, null, ['previous' => $oPrevious?->getMessage(), 'stack' => $e->getTraceAsString()]);
parent::__construct($sMessage, $iHttpCode, $oPrevious);
}
}

View File

@@ -202,10 +202,10 @@ class RunTimeEnvironment
if (!in_array($sModuleAppVersion, array('1.0.0', '1.0.1', '1.0.2')))
{
// This module is NOT compatible with the current version
$aModuleInfo['install'] = array(
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is not compatible with the current version of the application'
);
$aModuleInfo['install'] = array(
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is not compatible with the current version of the application'
);
}
elseif ($aModuleInfo['mandatory'])
{
@@ -457,19 +457,16 @@ class RunTimeEnvironment
{
if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect())
{
try
{
SetupInfo::SetSelectedModules($aRet);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($oModule->GetAutoSelect());
SetupInfo::SetSelectedModules($aRet);
try{
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($oModule->GetAutoSelect());
if ($bSelected)
{
$aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module
$bModuleAdded = true;
}
}
catch(Exception $e)
{
} catch(ModuleDiscoveryServiceException $e){
//do nothing. logged already
}
}
}
@@ -976,8 +973,8 @@ class RunTimeEnvironment
$this->CommitDir(
APPROOT.'env-'.$this->sTargetEnv,
APPROOT.'env-'.$this->sFinalEnv,
true,
false
true,
false
);
// Move the config file
@@ -1044,7 +1041,7 @@ class RunTimeEnvironment
* @param $sSource
* @param $sDest
* @param boolean $bSourceMustExist
* @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied
* @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied
* @throws Exception
*/
protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true)
@@ -1079,41 +1076,41 @@ class RunTimeEnvironment
}
}
/**
* Call the given handler method for all selected modules having an installation handler
* @param array[] $aAvailableModules
* @param string[] $aSelectedModules
* @param string $sHandlerName
* @throws CoreException
*/
/**
* Call the given handler method for all selected modules having an installation handler
* @param array[] $aAvailableModules
* @param string[] $aSelectedModules
* @param string $sHandlerName
* @throws CoreException
*/
public function CallInstallerHandlers($aAvailableModules, $aSelectedModules, $sHandlerName)
{
foreach($aAvailableModules as $sModuleId => $aModule)
{
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) &&
isset($aAvailableModules[$sModuleId]['installer']) )
{
$sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer'];
SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})");
$aCallSpec = array($sModuleInstallerClass, $sHandlerName);
if (is_callable($aCallSpec))
{
try {
call_user_func_array($aCallSpec, array(MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']));
} catch (Exception $e) {
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
'ModuleInstallerClass' => $sModuleInstallerClass,
'ModuleInstallerHandler' => $sHandlerName,
'ExceptionClass' => get_class($e),
'ExceptionMessage' => $e->getMessage(),
];
throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e);
}
}
}
}
foreach($aAvailableModules as $sModuleId => $aModule)
{
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) &&
isset($aAvailableModules[$sModuleId]['installer']) )
{
$sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer'];
SetupLog::Info("Calling Module Handler: $sModuleInstallerClass::$sHandlerName(oConfig, {$aModule['version_db']}, {$aModule['version_code']})");
$aCallSpec = array($sModuleInstallerClass, $sHandlerName);
if (is_callable($aCallSpec))
{
try {
call_user_func_array($aCallSpec, array(MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']));
} catch (Exception $e) {
$sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler";
$aExceptionContextData = [
'ModulelId' => $sModuleId,
'ModuleInstallerClass' => $sModuleInstallerClass,
'ModuleInstallerHandler' => $sHandlerName,
'ExceptionClass' => get_class($e),
'ExceptionMessage' => $e->getMessage(),
];
throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e);
}
}
}
}
}
/**
@@ -1142,64 +1139,64 @@ class RunTimeEnvironment
if ($aModule['version_db'] != '') {
// 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
{
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']);
}
}
}
}
}
$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
{
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
// the mapping between the identifiers in the XML and the actual identifiers
// in the current database
foreach($aPreviouslyLoadedFiles as $sFileRelativePath)
{
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)");
if (empty($sFileName) || !file_exists($sFileName))
{
throw(new Exception("File $sFileName does not exist"));
}
// Simulate the load of the previously loaded files, in order to initialize
// the mapping between the identifiers in the XML and the actual identifiers
// in the current database
foreach($aPreviouslyLoadedFiles as $sFileRelativePath)
{
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName (just to get the keys mapping)");
if (empty($sFileName) || !file_exists($sFileName))
{
throw(new Exception("File $sFileName does not exist"));
}
$oDataLoader->LoadFile($sFileName, true);
$sResult = sprintf("loading of %s done.", basename($sFileName));
SetupLog::Info($sResult);
}
$oDataLoader->LoadFile($sFileName, true);
$sResult = sprintf("loading of %s done.", basename($sFileName));
SetupLog::Info($sResult);
}
foreach($aFiles as $sFileRelativePath)
{
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName");
if (empty($sFileName) || !file_exists($sFileName))
{
throw(new Exception("File $sFileName does not exist"));
}
foreach($aFiles as $sFileRelativePath)
{
$sFileName = APPROOT.$sFileRelativePath;
SetupLog::Info("Loading file: $sFileName");
if (empty($sFileName) || !file_exists($sFileName))
{
throw(new Exception("File $sFileName does not exist"));
}
$oDataLoader->LoadFile($sFileName);
$sResult = sprintf("loading of %s done.", basename($sFileName));
SetupLog::Info($sResult);
}
$oDataLoader->LoadFile($sFileName);
$sResult = sprintf("loading of %s done.", basename($sFileName));
SetupLog::Info($sResult);
}
$oDataLoader->EndSession();
$oDataLoader->EndSession();
SetupLog::Info("ending data load session");
}
@@ -1212,12 +1209,12 @@ class RunTimeEnvironment
*/
protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge)
{
$aToMerge = array();
foreach($aFilesToMerge as $sFile)
{
$aToMerge[] = $sBaseDir.'/'.$sFile;
}
return array_merge($aSourceArray, $aToMerge);
$aToMerge = array();
foreach($aFilesToMerge as $sFile)
{
$aToMerge[] = $sBaseDir.'/'.$sFile;
}
return array_merge($aSourceArray, $aToMerge);
}
/**
@@ -1226,40 +1223,40 @@ class RunTimeEnvironment
* @throws Exception
* @return string
*/
public function CheckMetaModel()
{
$iCount = 0;
$fStart = microtime(true);
foreach(MetaModel::GetClasses() as $sClass)
{
if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass))
{
//if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity.
continue;
}
public function CheckMetaModel()
{
$iCount = 0;
$fStart = microtime(true);
foreach(MetaModel::GetClasses() as $sClass)
{
if (false == MetaModel::HasTable($sClass) && MetaModel::IsAbstract($sClass))
{
//if a class is not persisted and is abstract, the code below would crash. Needed by the class AbstractRessource. This is tolerable to skip this because we check the setup process integrity, not the datamodel integrity.
continue;
}
$oSearch = new DBObjectSearch($sClass);
$oSearch->SetShowObsoleteData(false);
$oSQLQuery = $oSearch->GetSQLQueryStructure(null, false);
$sViewName = MetaModel::DBGetView($sClass);
if (strlen($sViewName) > 64)
{
throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters).");
}
$sTableName = MetaModel::DBGetTable($sClass);
if (strlen($sTableName) > 64)
{
throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters).");
}
$iTableCount = $oSQLQuery->CountTables();
if ($iTableCount > 61)
{
throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation).");
}
$iCount++;
}
$fDuration = microtime(true) - $fStart;
$oSearch = new DBObjectSearch($sClass);
$oSearch->SetShowObsoleteData(false);
$oSQLQuery = $oSearch->GetSQLQueryStructure(null, false);
$sViewName = MetaModel::DBGetView($sClass);
if (strlen($sViewName) > 64)
{
throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters).");
}
$sTableName = MetaModel::DBGetTable($sClass);
if (strlen($sTableName) > 64)
{
throw new Exception("Table name too long for class: '$sClass'. The name of the corresponding MySQL table ($sTableName) would exceed MySQL's limit for the name of a table (64 characters).");
}
$iTableCount = $oSQLQuery->CountTables();
if ($iTableCount > 61)
{
throw new Exception("Class requiring too many tables: '$sClass'. The structure of the class ($sClass) would require a query with more than 61 JOINS (MySQL's limitation).");
}
$iCount++;
}
$fDuration = microtime(true) - $fStart;
return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0);
}
return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration*1000.0);
}
} // End of class

View File

@@ -270,14 +270,15 @@ class InstallationFileService {
{
try {
SetupInfo::SetSelectedModules($this->aSelectedModules);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']);
if ($bSelected)
{
// Modules in data/production-modules/ are considered as mandatory and always installed
$this->aSelectedModules[$sModuleId] = true;
}
}
catch (Exception $e) {
catch (ModuleDiscoveryServiceException $e) {
//logged already
}
}
}

View File

@@ -90,7 +90,7 @@ class WizStepWelcome extends WizardStep
$oPage->add("<!--[if lt IE 11]><div id=\"old_ie\"></div><![endif]-->");
$oPage->add_ready_script(
<<<EOF
<<<EOF
if ($('#old_ie').length > 0)
{
alert("Internet Explorer version 10 or older is NOT supported! (Check that IE is not running in compatibility mode)");
@@ -144,7 +144,7 @@ EOF
$sH2Class = 'text-valid';
}
$oPage->add(
<<<HTML
<<<HTML
<h2 class="message">Prerequisites validation: <span class="$sH2Class">$sTitle</span></h2>
<div id="details" $sStyle>
HTML
@@ -271,12 +271,12 @@ class WizStepInstallOrUpgrade extends WizardStep
}
$oPage->add('<div class="setup-content-title">What do you want to do?</div>');
$sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
$oPage->p('<input id="radio_install" type="radio" name="install_mode" value="install" '.$sChecked.'/><label for="radio_install">&nbsp;Install a new '.ITOP_APPLICATION.'</label>');
$oPage->p('<input id="radio_install" type="radio" name="install_mode" value="install" '.$sChecked.'/><label for="radio_install">&nbsp;Install a new '.ITOP_APPLICATION.'</label>');
$sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
$sDisabled = (($sInstallMode == 'install') && (empty($sPreviousVersionDir))) ? ' disabled' : '';
$oPage->p('<input id="radio_update" type="radio" name="install_mode" value="upgrade" '.$sChecked.$sDisabled.'/><label for="radio_update">&nbsp;Upgrade an existing '.ITOP_APPLICATION.' instance</label>');
$oPage->p('<input id="radio_update" type="radio" name="install_mode" value="upgrade" '.$sChecked.$sDisabled.'/><label for="radio_update">&nbsp;Upgrade an existing '.ITOP_APPLICATION.' instance</label>');
$sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir);
$sUpgradeDir = utils::HtmlEntities($sPreviousVersionDir);
$oPage->add(<<<HTML
<div id="upgrade_info"'.$sUpgradeInfoStyle.'>
<div class="setup-disk-location--input--container">Location on the disk:<input id="previous_version_dir_display" type="text" value="$sUpgradeDir" class="ibo-input" disabled>
@@ -319,7 +319,7 @@ HTML
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
//$oPage->add('</fieldset>');
$oPage->add_ready_script(
<<<JS
<<<JS
$("#radio_update").on('change', function() { if (this.checked ) { $('#upgrade_info').show(); WizardUpdateButtons(); } else { $('#upgrade_info').hide(); } });
$("#radio_install").on('change', function() { if (this.checked ) { $('#upgrade_info').hide(); WizardUpdateButtons(); } else { $('#upgrade_info').show(); } });
$("#db_backup_path").on('change keyup', function() { WizardAsyncAction('check_backup', { db_backup_path: $('#db_backup_path').val() }); });
@@ -350,12 +350,12 @@ JS
$("#db_pwd").trigger('change'); // Forces check of the DB connection
EOF
);
}
break;
}
break;
case 'check_db':
SetupUtils:: AsyncCheckDB($oPage, $aParameters);
break;
SetupUtils:: AsyncCheckDB($oPage, $aParameters);
break;
case 'check_backup':
$sDBBackupPath = $aParameters['db_backup_path'];
@@ -372,9 +372,9 @@ EOF
<<<EOF
$("#backup_info").html('');
EOF
);
}
break;
);
}
break;
}
}
@@ -385,7 +385,7 @@ EOF
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
if ($("#radio_install").prop("checked"))
{
ValidateField("db_name", false);
@@ -403,7 +403,7 @@ EOF
return bRet;
}
EOF
;
;
}
}
@@ -436,19 +436,19 @@ class WizStepDetectedInfo extends WizardStep
switch ($sUpgradeType)
{
case 'keep-previous':
$sSourceDir = utils::ReadParam('relative_source_dir', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir);
$this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data'));
break;
$sSourceDir = utils::ReadParam('relative_source_dir', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $this->oWizard->GetParameter('previous_version_dir').'/'.$sSourceDir);
$this->oWizard->SetParameter('datamodel_version', utils::ReadParam('datamodel_previous_version', '', false, 'raw_data'));
break;
case 'use-compatible':
$sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $sDataModelPath);
$this->oWizard->SaveParameter('datamodel_version', '');
break;
$sDataModelPath = utils::ReadParam('datamodel_path', '', false, 'raw_data');
$this->oWizard->SetParameter('source_dir', $sDataModelPath);
$this->oWizard->SaveParameter('datamodel_version', '');
break;
default:
// Do nothing, maybe the user pressed the Back button
// Do nothing, maybe the user pressed the Back button
}
if ($bDisplayLicense)
{
@@ -469,7 +469,7 @@ class WizStepDetectedInfo extends WizardStep
public function Display(WebPage $oPage)
{
$oPage->add_style(
<<<EOF
<<<EOF
#changes_summary {
max-height: 200px;
overflow: auto;
@@ -606,7 +606,7 @@ EOF
$sCompatibleDMDirToDisplay = utils::HtmlEntities($sCompatibleDMDir);
$sUpgradeDMVersionToDisplay = utils::HtmlEntities($sUpgradeDMVersion);
$oPage->add(
<<<HTML
<<<HTML
<div class="message message-valid">The datamodel will be upgraded from version $sInstalledDataModelVersion to version $sUpgradeDMVersion.</div>
<input type="hidden" name="upgrade_type" value="use-compatible">
<input type="hidden" name="datamodel_path" value="$sCompatibleDMDirToDisplay">
@@ -617,7 +617,7 @@ HTML
}
$oPage->add_ready_script(
<<<EOF
<<<EOF
$("#changes_summary .title").on('click', function() { $(this).parent().toggleClass('closed'); } );
$('input[name=upgrade_type]').on('click change', function() { WizardUpdateButtons(); });
EOF
@@ -650,13 +650,13 @@ EOF
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
if ($("#radio_upgrade_keep").length == 0) return true;
bRet = ($('input[name=upgrade_type]:checked').length > 0);
return bRet;
EOF
;
;
}
}
@@ -695,55 +695,55 @@ class WizStepLicense extends WizardStep
return (($sMode === 'install') && SetupUtils::IsConnectableToITopHub($aModules));
}
/**
* @param WebPage $oPage
*/
public function Display(WebPage $oPage)
{
$aLicenses = SetupUtils::GetLicenses();
$oPage->add_style(
<<<CSS
/**
* @param WebPage $oPage
*/
public function Display(WebPage $oPage)
{
$aLicenses = SetupUtils::GetLicenses();
$oPage->add_style(
<<<CSS
fieldset ul {
max-height: min(30em, 40vh); /* Allow usage of the UI up to 150% zoom */
overflow: auto;
}
CSS
);
);
$oPage->add('<h2>Licenses agreements for the components of '.ITOP_APPLICATION.'</h2>');
$oPage->add_style('div a.no-arrow { background:transparent; padding-left:0;}');
$oPage->add_style('.toggle { cursor:pointer; text-decoration:underline; color:#1C94C4; }');
$oPage->add('<fieldset>');
$oPage->add('<legend>Components of '.ITOP_APPLICATION.'</legend>');
$oPage->add('<ul id="ibo-setup-licenses--components-list">');
$index = 0;
foreach ($aLicenses as $oLicense) {
$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
$oPage->add('<div id="license_'.$index.'" class="license_text ibo-is-html-content" style="display:none;overflow:auto;max-height:10em;font-size:12px;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;"><pre>'.$oLicense->text.'</pre></div>');
$oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");');
$oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );');
$index++;
}
$oPage->add('</ul>');
$oPage->add('</fieldset>');
$sChecked = ($this->oWizard->GetParameter('accept_license', 'no') == 'yes') ? ' checked ' : '';
$oPage->add('<div class="setup-accept-licenses"><input class="check_select" type="checkbox" name="accept_license" id="accept" value="yes" '.$sChecked.'><label for="accept">I accept the terms of the licenses of the '.count($aLicenses).' components mentioned above.</label></div>');
if ($this->NeedsGdprConsent()) {
$oPage->add('<br>');
$oPage->add('<fieldset>');
$oPage->add('<legend>European General Data Protection Regulation</legend>');
$oPage->add('<div class="ibo-setup-licenses--components-list">'.ITOP_APPLICATION.' software is compliant with the processing of personal data according to the European General Data Protection Regulation (GDPR).<p></p>
$oPage->add('<h2>Licenses agreements for the components of '.ITOP_APPLICATION.'</h2>');
$oPage->add_style('div a.no-arrow { background:transparent; padding-left:0;}');
$oPage->add_style('.toggle { cursor:pointer; text-decoration:underline; color:#1C94C4; }');
$oPage->add('<fieldset>');
$oPage->add('<legend>Components of '.ITOP_APPLICATION.'</legend>');
$oPage->add('<ul id="ibo-setup-licenses--components-list">');
$index = 0;
foreach ($aLicenses as $oLicense) {
$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
$oPage->add('<div id="license_'.$index.'" class="license_text ibo-is-html-content" style="display:none;overflow:auto;max-height:10em;font-size:12px;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;"><pre>'.$oLicense->text.'</pre></div>');
$oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");');
$oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );');
$index++;
}
$oPage->add('</ul>');
$oPage->add('</fieldset>');
$sChecked = ($this->oWizard->GetParameter('accept_license', 'no') == 'yes') ? ' checked ' : '';
$oPage->add('<div class="setup-accept-licenses"><input class="check_select" type="checkbox" name="accept_license" id="accept" value="yes" '.$sChecked.'><label for="accept">I accept the terms of the licenses of the '.count($aLicenses).' components mentioned above.</label></div>');
if ($this->NeedsGdprConsent()) {
$oPage->add('<br>');
$oPage->add('<fieldset>');
$oPage->add('<legend>European General Data Protection Regulation</legend>');
$oPage->add('<div class="ibo-setup-licenses--components-list">'.ITOP_APPLICATION.' software is compliant with the processing of personal data according to the European General Data Protection Regulation (GDPR).<p></p>
By installing '.ITOP_APPLICATION.' you agree that some information will be collected by Combodo to help you manage your instances and for statistical purposes.
This data remains anonymous until it is associated to a user account on iTop Hub.</p>
<p>List of collected data available in our <a target="_blank" href="https://www.itophub.io/page/data-privacy">Data privacy section.</a></p><br></div>');
$oPage->add('<input type="checkbox" class="check_select" id="rgpd_consent">');
$oPage->add('<label for="rgpd_consent">&nbsp;I accept the processing of my personal data</label>');
$oPage->add('</fieldset>');
}
$oPage->add_ready_script('$(".check_select").on("click change", function() { WizardUpdateButtons(); });');
$oPage->add('<input type="checkbox" class="check_select" id="rgpd_consent">');
$oPage->add('<label for="rgpd_consent">&nbsp;I accept the processing of my personal data</label>');
$oPage->add('</fieldset>');
}
$oPage->add_ready_script('$(".check_select").on("click change", function() { WizardUpdateButtons(); });');
$oPage->add_script(
<<<JS
$oPage->add_script(
<<<JS
function isRgpdConsentOk(){
let eRgpdConsent = $("#rgpd_consent");
if(eRgpdConsent.length){
@@ -754,7 +754,7 @@ This data remains anonymous until it is associated to a user account on iTop Hub
return true;
}
JS
);
);
}
/**
@@ -850,8 +850,8 @@ class WizStepDBParams extends WizardStep
switch($sCode)
{
case 'check_db':
SetupUtils:: AsyncCheckDB($oPage, $aParameters);
break;
SetupUtils:: AsyncCheckDB($oPage, $aParameters);
break;
}
}
@@ -862,7 +862,7 @@ class WizStepDBParams extends WizardStep
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
if ($("#wiz_form").data("db_connection") === "error") return false;
var bRet = true;
@@ -872,7 +872,7 @@ class WizStepDBParams extends WizardStep
return bRet;
EOF
;
;
}
}
@@ -937,7 +937,7 @@ EOF
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
bRet = ($('#admin_user').val() != '');
if (!bRet)
{
@@ -1107,10 +1107,10 @@ EOF
default:
case CheckResult::ERROR:
case CheckResult::WARNING:
$sStatus = 'ko';
$sErrorExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-error">'.$sErrorExplanation.'</div>');
break;
$sStatus = 'ko';
$sErrorExplanation = $oCheck->sLabel;
$sMessage = json_encode('<div class="message message-error">'.$sErrorExplanation.'</div>');
break;
}
if ($oCheck->iSeverity !== CheckResult::TRACE) {
@@ -1134,7 +1134,7 @@ JS
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
bRet = ($('#application_url').val() != '');
if (!bRet)
{
@@ -1156,7 +1156,7 @@ JS
}
return bRet;
EOF
;
;
}
}
@@ -1261,7 +1261,7 @@ EOF
JS
);
}
break;
break;
}
}
@@ -1272,7 +1272,7 @@ JS
public function JSCanMoveForward()
{
return
<<<EOF
<<<EOF
bRet = ($('#application_url').val() != '');
if (!bRet)
{
@@ -1294,7 +1294,7 @@ JS
}
return bRet;
EOF
;
;
}
}
/**
@@ -1481,7 +1481,7 @@ class WizStepModulesChoice extends WizardStep
$oPage->add('</div>');
$oPage->add_script(
<<<EOF
<<<EOF
function CheckChoice(sChoiceId)
{
var oElement = $('#'+sChoiceId);
@@ -1530,7 +1530,7 @@ function CheckChoice(sChoiceId)
EOF
);
$oPage->add_ready_script(
<<<EOF
<<<EOF
$('.wiz-choice').on('change', function() { CheckChoice($(this).attr('id')); } );
$('.wiz-choice').trigger('change');
EOF
@@ -1787,16 +1787,18 @@ EOF
// Check the module selection
try {
SetupInfo::SetSelectedModules($aModules);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aInfo['auto_select']);
if ($bSelected) {
$aModules[$sModuleId] = true; // store the Id of the selected module
SetupInfo::SetSelectedModules($aModules);
}
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aInfo['auto_select']);
}
catch (Exception $e) {
catch (ModuleDiscoveryServiceException $e) {
//logged already
$bSelected = false;
}
}
}
if ($bSelected) {
$aModules[$sModuleId] = true; // store the Id of the selected module
SetupInfo::SetSelectedModules($aModules);
}
}
}
$sChoiceType = isset($aChoice['type']) ? $aChoice['type'] : 'wizard_option';
@@ -1823,7 +1825,7 @@ EOF
$sChoiceName = $sChoiceId;
}
if ( (isset($aChoice['mandatory']) && $aChoice['mandatory']) ||
(isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) )
(isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) )
{
$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
if ($aSelectedExtensions !== null)
@@ -1863,7 +1865,7 @@ EOF
try
{
SetupInfo::SetSelectedModules($aModules);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeAutoSelectExpression($aModule['auto_select']);
$bSelected = ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($aModule['auto_select']);
if ($bSelected)
{
$aModules[$sModuleId] = true; // store the Id of the selected module
@@ -1871,8 +1873,9 @@ EOF
$bModuleAdded = true;
}
}
catch(Exception $e)
catch(ModuleDiscoveryServiceException $e)
{
//logged already
$sDisplayChoices .= '<li><b>Warning: auto_select failed with exception ('.$e->getMessage().') for module "'.$sModuleId.'"</b></li>';
}
}
@@ -1890,11 +1893,11 @@ EOF
{
case 'start_install':
case 'start_upgrade':
$index = 0;
break;
$index = 0;
break;
default:
$index = (integer)$this->sCurrentState;
$index = (integer)$this->sCurrentState;
}
return $index;
}
@@ -1921,10 +1924,10 @@ EOF
// Additional step for the "extensions"
$aStepDefinition = array(
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => array()
'title' => 'Extensions',
'description' => '<h2>Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.</h2>',
'banner' => '/images/icons/icons8-puzzle.svg',
'options' => array()
);
foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension)
@@ -1932,14 +1935,14 @@ EOF
if (($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) && ($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0))
{
$aStepDefinition['options'][] = array(
'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),
'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),
);
}
}
@@ -1954,24 +1957,24 @@ EOF
{
// No wizard configuration provided, build a standard one with just one big list
$aStepDefinition = array(
'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' => array()
'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' => array()
);
foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension)
{
if (($oExtension->bVisible) && (count($oExtension->aMissingDependencies) == 0))
{
$aStepDefinition['options'][] = array(
'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),
'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),
);
}
}
@@ -1992,17 +1995,17 @@ EOF
switch($sSource)
{
case iTopExtension::SOURCE_MANUAL:
$sResult = 'Local extensions folder';
$sDecorationClass = 'fas fa-folder';
break;
$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;
$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop Hub' : 'ITSM Designer';
$sDecorationClass = (ITOP_APPLICATION == 'iTop') ? 'fc fc-chameleon-icon' : 'fa pencil-ruler';
break;
default:
$sResult = '';
$sResult = '';
}
if ($sResult == '')
{
@@ -2587,10 +2590,10 @@ class WizStepDone extends WizardStep
$oProductionEnv->InitDataModel($oConfig, true);
$sIframeUrl = $oConfig->GetModuleSetting('itop-hub-connector', 'setup_url', '');
$sSetupTokenFile = APPROOT.'data/.setup';
$sSetupToken = bin2hex(random_bytes(12));
file_put_contents($sSetupTokenFile, $sSetupToken);
$sIframeUrl.= "&setup_token=$sSetupToken";
$sSetupTokenFile = APPROOT.'data/.setup';
$sSetupToken = bin2hex(random_bytes(12));
file_put_contents($sSetupTokenFile, $sSetupToken);
$sIframeUrl.= "&setup_token=$sSetupToken";
if ($sIframeUrl != '')
{

View File

@@ -0,0 +1,55 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Setup\ModuleDiscovery;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ModuleDiscoveryService;
class ModuleDiscoveryServiceTest extends ItopDataTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('setup/modulediscovery/ModuleDiscoveryService.php');
}
public function test()
{
$sModuleFilePath = __DIR__.'/resources/module.itop-full-itil.php';
$aRes = ModuleDiscoveryService::GetInstance()->ReadModuleFileConfiguration($sModuleFilePath);
var_dump($aRes);
$this->assertCount(3, $aRes);
$this->assertEquals($sModuleFilePath, $aRes[0]);
$this->assertEquals('itop-full-itil/3.3.0', $aRes[1]);
$this->assertIsArray($aRes[2]);
$this->assertArrayHasKey('label', $aRes[2]);
$this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null);
}
public static function ComputeBooleanExpressionProvider()
{
return [
"true" => [ "expr" => "true", "expected" => true],
"(true)" => [ "expr" => "(true)", "expected" => true],
"(false||true)" => [ "expr" => "(false||true)", "expected" => true],
"false" => [ "expr" => "false", "expected" => false],
"(false)" => [ "expr" => "(false)", "expected" => false],
"(false&&true)" => [ "expr" => "(false&&true)", "expected" => false],
];
}
/**
* @dataProvider ComputeBooleanExpressionProvider
*/
public function testComputeBooleanExpression(string $sBooleanExpression, bool $expected){
$this->assertEquals($expected, ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression($sBooleanExpression), $sBooleanExpression);
}
public function testComputeBooleanExpression_BrokenBooleanExpression(){
$this->expectException(\ModuleDiscoveryServiceException::class);
$this->expectExceptionMessage('Eval of \'(a || true)\' caused an error: Undefined constant "a"');
$this->assertTrue(ModuleDiscoveryService::GetInstance()->ComputeBooleanExpression("(a || true)"));
}
}

View File

@@ -0,0 +1,41 @@
<?php
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-full-itil/3.3.0',
array(
// Identification
//
'label' => 'Bridge - Request management ITIL + Incident management ITIL',
'category' => 'business',
// Setup
//
'dependencies' => array(
'itop-request-mgmt-itil/2.3.0',
'itop-incident-mgmt-itil/2.3.0',
),
'mandatory' => false,
'visible' => false,
'auto_select' => 'SetupInfo::ModuleIsSelected("itop-request-mgmt-itil") && SetupInfo::ModuleIsSelected("itop-incident-mgmt-itil")',
// Components
//
'datamodel' => array(),
'webservice' => array(),
'data.struct' => array(// add your 'structure' definition XML files here,
),
'data.sample' => array(// add your sample data XML files here,
),
// Documentation
//
'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
'doc.more_information' => '', // hyperlink to more information, if any
// Default settings
//
'settings' => array(// Module specific settings go here, if any
),
)
);