From d0d9b1ce504a41613ce574eae13860fd05ee0954 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Fri, 7 Jul 2017 16:00:30 +0000 Subject: [PATCH] Improved way to track the choices made during the installation in order to: 1) Be able to proerly report this information 2) Make sure that the same (proper) choices are proposed upon update SVN:trunk[4815] --- css/light-grey.css | 8 + css/light-grey.scss | 8 + dictionaries/dictionary.itop.ui.php | 5 +- dictionaries/fr.dictionary.itop.ui.php | 5 +- pages/ajax.render.php | 49 ++- setup/applicationinstaller.class.inc.php | 25 +- setup/extensionsmap.class.inc.php | 458 +++++++++++++++++++++++ setup/modelfactory.class.inc.php | 8 +- setup/modulediscovery.class.inc.php | 14 +- setup/moduleinstallation.class.inc.php | 48 ++- setup/runtimeenv.class.inc.php | 77 +++- setup/wizardsteps.class.inc.php | 376 ++++++++++++++----- 12 files changed, 938 insertions(+), 143 deletions(-) create mode 100644 setup/extensionsmap.class.inc.php diff --git a/css/light-grey.css b/css/light-grey.css index 05430e638..30b5f5f6a 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -1962,3 +1962,11 @@ span.refresh-button { .object-ref-link { background: none; } +.extension-source { + display: inline-block; + background-color: #555; + padding: 3px; + font-size: 10px; + color: #fff; + border-radius: 4px; +} diff --git a/css/light-grey.scss b/css/light-grey.scss index 828b56675..79c04a5d1 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -2163,3 +2163,11 @@ span.refresh-button { .object-ref-link { background: none; } +.extension-source { + display:inline-block; + background-color: $grey-color; + padding:3px; + font-size:10px; + color:#fff; + border-radius: 4px; +} \ No newline at end of file diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index 034450175..4371d62e2 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -1305,7 +1305,10 @@ When associated with a trigger, each action is given an "order" number, specifyi 'UI:About:DataModel' => 'Data model', 'UI:About:Support' => 'Support information', 'UI:About:Licenses' => 'Licenses', - 'UI:About:Modules' => 'Installed modules', + 'UI:About:InstallationOptions' => 'Installation options', + 'UI:About:ManualExtensionSource' => 'Extension', + 'UI:About:Extension_Version' => 'Version: %1$s', + 'UI:About:RemoteExtensionSource' => 'Data', 'UI:DisconnectedDlgMessage' => 'You are disconnected. You must identify yourself to continue using the application.', 'UI:DisconnectedDlgTitle' => 'Warning!', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 9288e1b24..7bb092510 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -1148,7 +1148,10 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé 'UI:About:DataModel' => 'Modèle de données', 'UI:About:Support' => 'Informations pour le support', 'UI:About:Licenses' => 'Licences', - 'UI:About:Modules' => 'Modules installés', + 'UI:About:InstallationOptions' => 'Options d\'installation', + 'UI:About:Extension_Version' => 'Version: %1$s', + 'UI:About:ManualExtensionSource' => 'Extension', + 'UI:DisconnectedDlgMessage' => 'Vous êtes déconnecté(e). Vous devez vous identifier pour pouvoir continuer à utiliser l\'application.', 'UI:DisconnectedDlgTitle' => 'Attention !', diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 4f58939c0..206db5777 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -1231,16 +1231,30 @@ EOF $oPage->add(""); $oPage->add('
'); - $oPage->add(''.Dict::S('UI:About:Modules').''); - //$oPage->add(print_r($aAvailableModules, true)); - $oPage->add("
"); + $oPage->add(''.Dict::S('UI:About:InstallationOptions').''); + $oPage->add("
"); $oPage->add('
    '); - foreach ($aAvailableModules as $sModuleId => $aModuleData) + + require_once(APPROOT.'setup/extensionsmap.class.inc.php'); + $oExtensionsMap = new iTopExtensionsMap(); + $oExtensionsMap->LoadChoicesFromDatabase(MetaModel::GetConfig()); + $aChoices = $oExtensionsMap->GetChoices(); + foreach ($aChoices as $oExtension) { - if ($sModuleId == '_Root_') continue; - if (!$aModuleData['visible']) continue; - if ($aModuleData['version_db'] == '') continue; - $oPage->add('
  • '.$aModuleData['label'].' ('.$aModuleData['version_db'].')
  • '); + switch($oExtension->sSource) + { + case iTopExtension::SOURCE_REMOTE: + $sSource = ' '.Dict::S('UI:About:RemoteExtensionSource').''; + break; + + case iTopExtension::SOURCE_MANUAL: + $sSource = ' '.Dict::S('UI:About:ManualExtensionSource').''; + break; + + default: + $sSource = ''; + } + $oPage->add('
  • '.$oExtension->sLabel.$sSource.'
  • '); } $oPage->add('
'); $oPage->add("
"); @@ -1280,6 +1294,25 @@ EOF $oPage->add('InstallDate: '.$sLastInstallDate."\n"); $oPage->add('InstallPath: '.APPROOT."\n"); + $oPage->add("---- Installation choices ----\n"); + foreach ($aChoices as $oExtension) + { + switch($oExtension->sSource) + { + case iTopExtension::SOURCE_REMOTE: + $sSource = ' ('.Dict::S('UI:About:RemoteExtensionSource').')'; + break; + + case iTopExtension::SOURCE_MANUAL: + $sSource = ' ('.Dict::S('UI:About:ManualExtensionSource').')'; + break; + + default: + $sSource = ''; + } + $oPage->add('InstalledExtension/'.$oExtension->sCode.'/'.$oExtension->sVersion.$sSource."\n"); + } + $oPage->add("---- Actual modules installed ----\n"); foreach ($aAvailableModules as $sModuleId => $aModuleData) { if ($sModuleId == '_Root_') continue; diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php index e2de6f5cb..1c3484652 100644 --- a/setup/applicationinstaller.class.inc.php +++ b/setup/applicationinstaller.class.inc.php @@ -198,7 +198,6 @@ class ApplicationInstaller $sTargetEnvironment = 'production'; } $sTargetDir = 'env-'.$sTargetEnvironment; - $sWorkspaceDir = $this->oParams->Get('workspace_dir', 'workspace'); $bUseSymbolicLinks = false; $aMiscOptions = $this->oParams->Get('options', array()); if (isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'] ) @@ -299,7 +298,6 @@ class ApplicationInstaller $sDBPwd = $aDBParams['pwd']; $sDBName = $aDBParams['name']; $sDBPrefix = $aDBParams['prefix']; - $aFiles = $this->oParams->Get('files', array()); $bOldAddon = $this->oParams->Get('old_addon', false); $bSampleData = ($this->oParams->Get('sample_data', 0) == 1); @@ -331,13 +329,14 @@ class ApplicationInstaller $sUrl = $this->oParams->Get('url', ''); $sGraphvizPath = $this->oParams->Get('graphviz_path', ''); $sLanguage = $this->oParams->Get('language', ''); - $aSelectedModules = $this->oParams->Get('selected_modules', array()); + $aSelectedModuleCodes = $this->oParams->Get('selected_modules', array()); + $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', array()); $bOldAddon = $this->oParams->Get('old_addon', false); $sSourceDir = $this->oParams->Get('source_dir', ''); $sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', ''); $sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0'); - self::DoCreateConfig($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile, $sDataModelVersion, $sGraphvizPath); + self::DoCreateConfig($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile, $sDataModelVersion, $sGraphvizPath); $aResult = array( 'status' => self::INFO, @@ -491,7 +490,7 @@ class ApplicationInstaller $aModules = $oFactory->FindModules(); - foreach($aModules as $foo => $oModule) + foreach($aModules as $oModule) { $sModule = $oModule->GetName(); if (in_array($sModule, $aSelectedModules)) @@ -531,8 +530,8 @@ class ApplicationInstaller SetupPage::log_info("Data model successfully compiled to '$sTargetPath'."); $sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/'; - Setuputils::builddir($sCacheDir); - Setuputils::tidydir($sCacheDir); + SetupUtils::builddir($sCacheDir); + SetupUtils::tidydir($sCacheDir); } // Special case to patch a ugly patch in itop-config-mgmt @@ -548,7 +547,7 @@ class ApplicationInstaller // Set an "Instance UUID" identifying this machine based on a file located in the data directory $sInstanceUUIDFile = APPROOT.'data/instance.txt'; - Setuputils::builddir(APPROOT.'data'); + SetupUtils::builddir(APPROOT.'data'); if (!file_exists($sInstanceUUIDFile)) { $sIntanceUUID = utils::CreateUUID('filesystem'); @@ -690,7 +689,8 @@ class ApplicationInstaller // Syncho data sources were identified by the comment at the end // Unfortunately the comment is localized, so we have to search for all possible patterns $sCurrentLanguage = Dict::GetUserLanguage(); - foreach(Dict::GetLanguages() as $sLangCode => $aLang) + $aSuffixes = array(); + foreach(array_keys(Dict::GetLanguages()) as $sLangCode) { Dict::SetUserLanguage($sLangCode); $sSuffix = CMDBSource::Quote('%'.Dict::S('Core:SyncDataExchangeComment')); @@ -966,7 +966,7 @@ class ApplicationInstaller SetupPage::log_info("ending data load session"); } - protected static function DoCreateConfig($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile, $sDataModelVersion, $sGraphvizPath) + protected static function DoCreateConfig($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile, $sDataModelVersion, $sGraphvizPath) { $aParamValues = array( 'mode' => $sMode, @@ -979,7 +979,7 @@ class ApplicationInstaller 'application_path' => $sUrl, 'language' => $sLanguage, 'graphviz_path' => $sGraphvizPath, - 'selected_modules' => implode(',', $aSelectedModules), + 'selected_modules' => implode(',', $aSelectedModuleCodes) ); $bPreserveModuleSettings = false; @@ -1024,8 +1024,7 @@ class ApplicationInstaller // Record which modules are installed... $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment); $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database - $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir); - if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $sModulesDir)) + if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sModulesDir)) { throw new Exception("Failed to record the installation information"); } diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php new file mode 100644 index 000000000..64b951f33 --- /dev/null +++ b/setup/extensionsmap.class.inc.php @@ -0,0 +1,458 @@ +sCode = ''; + $this->sLabel = ''; + $this->sDescription = ''; + $this->sSource = self::SOURCE_WIZARD; + $this->bMandatory = false; + $this->sMoreInfoUrl = ''; + $this->bMarkedAsChosen = false; + $this->sVersion = ITOP_VERSION; + $this->sInstalledVersion = ''; + } +} + +/** + * Helper class to discover all available extensions on a given iTop system + */ +class iTopExtensionsMap +{ + /** + * The list of all discovered extensions + * @var iTopExtension[] + */ + protected $aExtensions; + + public function __construct($sFromEnvironment = 'production') + { + $this->aExtensions = array(); + $this->ScanDisk($sFromEnvironment); + } + + /** + * Populate the list of available (pseudo)extensions by scanning the disk + * where the iTop files are located + * @param string $sEnvironment + * @return void + */ + protected function ScanDisk($sEnvironment) + { + if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x') && !$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) + { + if(!$this->ReadDir(APPROOT.'/datamodels/2.x', iTopExtension::SOURCE_WIZARD)) $this->ReadDir(APPROOT.'/datamodels/1.x', iTopExtension::SOURCE_WIZARD); + } + $this->ReadDir(APPROOT.'/extensions', iTopExtension::SOURCE_MANUAL); + $this->ReadDir(APPROOT.'/data/'.$sEnvironment.'-modules', iTopExtension::SOURCE_REMOTE); + } + + /** + * Read the information contained in the "installation.xml" file in the given directory + * and create pseudo extensions from the list of choices described in this file + * @param string $sDir + * @return boolean Return true if the installation.xml file exists and is readable + */ + protected function ReadInstallationWizard($sDir) + { + if (!is_readable($sDir.'/installation.xml')) return false; + + $oXml = new XMLParameters($sDir.'/installation.xml'); + foreach($oXml->Get('steps') as $aStepInfo) + { + if (array_key_exists('options', $aStepInfo)) + { + $this->ProcessWizardChoices($aStepInfo['options']); + } + if (array_key_exists('alternatives', $aStepInfo)) + { + $this->ProcessWizardChoices($aStepInfo['alternatives']); + } + } + return true; + } + + /** + * Helper to process a "choice" array read from the installation.xml file + * @param array $aChoices + * @return void + */ + protected function ProcessWizardChoices($aChoices) + { + foreach($aChoices as $aChoiceInfo) + { + if (array_key_exists('extension_code', $aChoiceInfo)) + { + $oExtension = new iTopExtension(); + $oExtension->sCode = $aChoiceInfo['extension_code']; + $oExtension->sLabel = $aChoiceInfo['title']; + if (array_key_exists('modules', $aChoiceInfo)) + { + // Some wizard choices are not associated with any module + $oExtension->aModules = $aChoiceInfo['modules']; + } + if (array_key_exists('sub_options', $aChoiceInfo)) + { + if (array_key_exists('options', $aChoiceInfo['sub_options'])) + { + $this->ProcessWizardChoices($aChoiceInfo['sub_options']['options']); + } + if (array_key_exists('alternatives', $aChoiceInfo['sub_options'])) + { + $this->ProcessWizardChoices($aChoiceInfo['sub_options']['alternatives']); + } + } + $this->AddExtension($oExtension); + } + } + } + + /** + * Add an extension to the list of existing extensions, taking care of removing duplicates + * (only the latest/greatest version is kept) + * @param iTopExtension $oNewExtension + * @return void + */ + protected function AddExtension(iTopExtension $oNewExtension) + { + foreach($this->aExtensions as $key => $oExtension) + { + if ($oExtension->sCode == $oNewExtension->sCode) + { + if (version_compare($oNewExtension->sVersion, $oExtension->sVersion, '>')) + { + // This "new" extension is "newer" than the previous one, let's replace the previous one + unset($this->aExtensions[$key]); + $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; + return; + } + else + { + // This "new" extension is not "newer" than the previous one, let's ignore it + return; + } + } + } + // Finally it's not a duplicate, let's add it to the list + $this->aExtensions[$oNewExtension->sCode.'/'.$oNewExtension->sVersion] = $oNewExtension; + } + + /** + * Read (recursively) a directory to find if it contains extensions (or modules) + * @param string $sSearchDir The directory to scan + * @param string $sSource The 'source' value for the extensions found in this directory + * @param string|null $sParentExtensionId Not null if the directory is under a declared extension + * @return boolean + */ + protected function ReadDir($sSearchDir, $sSource, $sParentExtensionId = null) + { + if (!is_readable($sSearchDir)) return false; + $hDir = opendir($sSearchDir); + if ($hDir !== false) + { + $sExtensionId = null; + $aSubDirectories = array(); + + // First check if there is an extension.xml file in this directory + if (is_readable($sSearchDir.'/extension.xml')) + { + $oXml = new XMLParameters($sSearchDir.'/extension.xml'); + $oExtension = new iTopExtension(); + $oExtension->sCode = $oXml->Get('extension_code'); + $oExtension->sLabel = $oXml->Get('label'); + $oExtension->sDescription = $oXml->Get('description'); + $oExtension->sVersion = $oXml->Get('version'); + $oExtension->bMandatory = ($oXml->Get('mandatory') == 'true'); + $oExtension->sMoreInfoUrl = $oXml->Get('more_info_url'); + $oExtension->sVersion = $oXml->Get('version'); + $oExtension->sSource = $sSource; + + $sParentExtensionId = $sExtensionId = $oExtension->sCode.'/'.$oExtension->sVersion; + $this->AddExtension($oExtension); + } + // Then scan the other files and subdirectories + while (($sFile = readdir($hDir)) !== false) + { + if (($sFile !== '.') && ($sFile !== '..')) + { + $aMatches = array(); + if (is_dir($sSearchDir.'/'.$sFile)) + { + // Recurse after parsing all the regular files + $aSubDirectories[] = $sSearchDir.'/'.$sFile; + } + else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) + { + // Found a module + $aModuleInfo = $this->GetModuleInfo($sSearchDir.'/'.$sFile); + // If we are not already inside a formal extension, then the module itself is considered + // as an extension, otherwise, the module is just added to the list of modules belonging + // to this extension + $sModuleId = $aModuleInfo[1]; + list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); + + if ($sParentExtensionId !== null) + { + // Already inside an extension, let's add this module the list of modules belonging to this extension + $this->aExtensions[$sParentExtensionId]->aModules[] = $sModuleName; + } + else + { + // Not already inside an folder containing an 'extension.xml' file + + // Ignore non-visible modules and auto-select ones, since these are never prompted + // as a choice to the end-user + if (!$aModuleInfo[2]['visible'] || isset($aModuleInfo[2]['auto_select'])) continue; + + // Let's create a "fake" extension from this module (containing just this module) for backwards compatibility + $sExtensionId = $sModuleId; + + $oExtension = new iTopExtension(); + $oExtension->sCode = $sModuleName; + $oExtension->sLabel = $aModuleInfo[2]['label']; + $oExtension->sDescription = ''; + $oExtension->sVersion = $sModuleVersion; + $oExtension->sSource = $sSource; + $oExtension->bMandatory = $aModuleInfo[2]['mandatory']; + $oExtension->sMoreInfoUrl = $aModuleInfo[2]['doc.more_information']; + $oExtension->aModules = array($sModuleName); + + $this->AddExtension($oExtension); + + } + } + } + } + closedir($hDir); + foreach($aSubDirectories as $sDir) + { + // Recurse inside the subdirectories + $this->ReadDir($sDir, $sSource, $sExtensionId); + } + return true; + } + return false; + } + + /** + * Read the information from a module file (module.xxx.php) + * Closely inspired (almost copied/pasted !!) from ModuleDiscovery::ListModuleFiles + * @param string $sModuleFile + * @return array + */ + protected function GetModuleInfo($sModuleFile) + { + static $iDummyClassIndex = 0; + + $aModuleInfo = array(); // will be filled by the "eval" line below... + try + { + $aMatches = array(); + $sModuleFileContents = file_get_contents($sModuleFile); + $sModuleFileContents = str_replace(array(''), '', $sModuleFileContents); + $sModuleFileContents = str_replace('__FILE__', "'".addslashes($sModuleFile)."'", $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 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); + } + $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) + { + SetupPage::log_warning("Eval of $sModuleFile did not return the expected information..."); + } + } + catch(Exception $e) + { + // Continue... + SetupPage::log_warning("Eval of $sModuleFile caused an exception: ".$e->getMessage()); + } + return $aModuleInfo; + } + + /** + * Get all available extensions + * @return iTopExtension[] + */ + public function GetAllExtensions() + { + return $this->aExtensions; + } + + /** + * Mark the given extension as chosen + * @param string $sExtensionCode The code of the extension (code without verison number) + * @param bool $bMark The value to set for the bmarkAschosen flag + * @return void + */ + public function MarkAsChosen($sExtensionCode, $bMark = true) + { + foreach($this->aExtensions as $oExtension) + { + if ($oExtension->sCode == $sExtensionCode) + { + $oExtension->bMarkedAsChosen = $bMark; + break; + } + } + } + + /** + * Tells if a given extension(code) is marked as chosen + * @param string $sExtensionCode + * @return boolean + */ + public function IsMarkedAsChosen($sExtensionCode) + { + foreach($this->aExtensions as $oExtension) + { + if ($oExtension->sCode == $sExtensionCode) + { + return $oExtension->bMarkedAsChosen; + } + } + return false; + } + + /** + * Set the 'installed_version' of the given extension(code) + * @param string $sExtensionCode + * @param string $sInstalledVersion + * @return void + */ + protected function SetInstalledVersion($sExtensionCode, $sInstalledVersion) + { + foreach($this->aExtensions as $oExtension) + { + if ($oExtension->sCode == $sExtensionCode) + { + $oExtension->sInstalledVersion = $sInstalledVersion; + break; + } + } + } + + /** + * Get the list of the "chosen" extensions + * @return iTopExtension[] + */ + public function GetChoices() + { + $aResult = array(); + foreach($this->aExtensions as $oExtension) + { + if ($oExtension->bMarkedAsChosen) + { + $aResult[] = $oExtension; + } + } + return $aResult; + } + + /** + * Load the choices (i.e. MarkedAsChosen) from the database defined in the supplied Config + * @param Config $oConfig + * @return bool + */ + public function LoadChoicesFromDatabase(Config $oConfig) + { + $aInstalledExtensions = array(); + try + { + if (CMDBSource::DBName() === null) + { + CMDBSource::Init($oConfig->GetDBHost(), $oConfig->GetDBUser(), $oConfig->GetDBPwd(), $oConfig->GetDBName()); + CMDBSource::SetCharacterSet($oConfig->GetDBCharacterSet(), $oConfig->GetDBCollation()); + } + $sLatestInstallationDate = CMDBSource::QueryToScalar("SELECT max(installed) FROM ".$oConfig->GetDBSubname()."priv_extension_install"); + $aInstalledExtensions = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->GetDBSubname()."priv_extension_install WHERE installed = '".$sLatestInstallationDate."'"); + } + catch (MySQLException $e) + { + // No database or erroneous information + $aInstalledExtensions = array(); + return false; + } + + foreach($aInstalledExtensions as $aDBInfo) + { + $this->MarkAsChosen($aDBInfo['code']); + $this->SetInstalledVersion($aDBInfo['code'], $aDBInfo['version']); + } + return true; + } +} \ No newline at end of file diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php index 78acc5e2b..d3bf56f5c 100644 --- a/setup/modelfactory.class.inc.php +++ b/setup/modelfactory.class.inc.php @@ -220,13 +220,19 @@ class MFModule public function SetFilesToInclude($aFiles, $sCategory) { + // Now ModuleDiscovery provides us directly with relative paths... nothing to do + $this->aFilesToInclude[$sCategory] = $aFiles; + + /* $sDir = basename($this->sRootDir); $iLen = strlen($sDir.'/'); foreach($aFiles as $sFile) { $iPos = strpos($sFile, $sDir.'/'); - $this->aFilesToInclude[$sCategory][] = substr($sFile, $iPos+$iLen); + //$this->aFilesToInclude[$sCategory][] = substr($sFile, $iPos+$iLen); + $this->aFilesToInclude[$sCategory][] = $sFile; } + */ } public function GetFilesToInclude($sCategory) diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 490b14e6a..edf61ae56 100644 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -67,7 +67,7 @@ class ModuleDiscovery // Assume 1.0.2 $aArgs['itop_version'] = '1.0.2'; } - foreach (self::$m_aModuleArgs as $sArgName => $sArgDesc) + foreach (array_keys(self::$m_aModuleArgs) as $sArgName) { if (!array_key_exists($sArgName, $aArgs)) { @@ -110,6 +110,8 @@ class ModuleDiscovery self::$m_aModules[$sId] = $aArgs; + // Now keep the relative paths, as provided + /* foreach(self::$m_aFilesList as $sAttribute) { if (isset(self::$m_aModules[$sId][$sAttribute])) @@ -122,7 +124,9 @@ class ModuleDiscovery } } } + */ // Populate automatically the list of dictionary files + $aMatches = array(); if(preg_match('|^([^/]+)|', $sId, $aMatches)) // ModuleName = everything before the first forward slash { $sModuleName = $aMatches[1]; @@ -240,6 +244,7 @@ class ModuleDiscovery // Separate the module names from their version for an easier comparison later foreach($aOrderedModules as $sModuleId) { + $aMatches = array(); if (preg_match('|^([^/]+)/(.*)$|', $sModuleId, $aMatches)) { $aModuleVersions[$aMatches[1]] = $aMatches[2]; @@ -260,6 +265,7 @@ class ModuleDiscovery { // $sModuleId in the dependency string is made of a / // where the operator is < <= = > >= (by default >=) + $aModuleMatches = array(); if(preg_match('|^([^/]+)/(?=?)([^><=]+)$|', $sModuleId, $aModuleMatches)) { $sModuleName = $aModuleMatches[1]; @@ -295,7 +301,7 @@ class ModuleDiscovery } } $bMissingPrerequisite = false; - foreach ($aPotentialPrerequisites as $sModuleName => $void) + foreach (array_keys($aPotentialPrerequisites) as $sModuleName) { if (array_key_exists($sModuleName, $aSelectedModules)) { @@ -378,6 +384,7 @@ class ModuleDiscovery */ public static function GetModuleName($sModuleId) { + $aMatches = array(); if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) { $sName = $aMatches[1]; @@ -394,12 +401,12 @@ class ModuleDiscovery /** * Helper function to browse a directory and get the modules * @param $sRelDir string Directory to start from + * @param $sRootDir string The root directory path * @return array(name, version) */ protected static function ListModuleFiles($sRelDir, $sRootDir) { static $iDummyClassIndex = 0; - static $aDefinedClasses = array(); $sDirectory = $sRootDir.'/'.$sRelDir; if ($hDir = opendir($sDirectory)) @@ -504,3 +511,4 @@ class SetupWebPage extends ModuleDiscovery */ class DummyHandler { } + diff --git a/setup/moduleinstallation.class.inc.php b/setup/moduleinstallation.class.inc.php index 8b287810f..fc5d2bb27 100644 --- a/setup/moduleinstallation.class.inc.php +++ b/setup/moduleinstallation.class.inc.php @@ -1,5 +1,5 @@ +/** + * Persistent class ExtensionInstallation to record the installed extensions + * Log of extensions installations + * + * @copyright Copyright (C) 2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class ExtensionInstallation extends cmdbAbstractObject +{ + public static function Init() + { + $aParams = array + ( + "category" => "core,view_in_gui", + "key_type" => "autoincrement", + "name_attcode" => "", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_extension_install", + "db_key_field" => "id", + "db_finalclass_field" => "", + "display_template" => "", + ); + MetaModel::Init_Params($aParams); + //MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeString("code", array("allowed_values"=>null, "sql"=>"code", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("label", array("allowed_values"=>null, "sql"=>"label", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeString("source", array("allowed_values"=>null, "sql"=>"source", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("installed", array("allowed_values"=>null, "sql"=>"installed", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + + + // Display lists + MetaModel::Init_SetZListItems('details', array('code', 'label', 'version', 'installed', 'source')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('code', 'label', 'version', 'installed', 'source')); // Attributes to be displayed for a list + MetaModel::Init_SetZListItems('standard_search', array('code', 'label', 'version', 'installed', 'source')); // Attributes to be displayed in the search form + } +} + + diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index d4b126c4a..c741625d6 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -27,6 +27,7 @@ require_once(APPROOT."setup/modulediscovery.class.inc.php"); require_once(APPROOT.'setup/modelfactory.class.inc.php'); require_once(APPROOT.'setup/compiler.class.inc.php'); +require_once(APPROOT.'setup/extensionsmap.class.inc.php'); require_once(APPROOT.'core/metamodel.class.php'); define ('MODULE_ACTION_OPTIONAL', 1); @@ -41,9 +42,16 @@ class RunTimeEnvironment { protected $sTargetEnv; + /** + * Extensions map of the source environment + * @var iTopExtensionsMap + */ + protected $oExtensionsMap; + public function __construct($sEnvironment = 'production') { $this->sTargetEnv = $sEnvironment; + $this->oExtensionsMap = null; } /** @@ -104,6 +112,11 @@ class RunTimeEnvironment } MetaModel::Startup($oConfig, $bModelOnly, $bUseCache, false /* $bTraceSourceFiles */, $this->sTargetEnv); + + if ($this->oExtensionsMap === null) + { + $this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv); + } } /** @@ -333,11 +346,26 @@ class RunTimeEnvironment $aRet = array(); - // Determine the installed modules + // Determine the installed modules and extensions // $oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE); $oSourceEnv = new RunTimeEnvironment($sSourceEnv); $aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile); + + // Actually read the modules available for the target environment, + // but get the selection from the source environment and finally + // mark as (automatically) chosen alll the "remote" modules present in the + // target environment (data/-modules) + // The actual choices will be recorded by RecordInstallation below + $this->oExtensionsMap = new iTopExtensionsMap($this->sTargetEnv); + $this->oExtensionsMap->LoadChoicesFromDatabase($oSourceConfig); + foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) + { + if($oExtension->sSource == iTopExtension::SOURCE_REMOTE) + { + $this->oExtensionsMap->MarkAsChosen($oExtension->sCode); + } + } // Do load the required modules // @@ -359,7 +387,7 @@ class RunTimeEnvironment } $aModules = $oFactory->FindModules(); - foreach($aModules as $foo => $oModule) + foreach($aModules as $oModule) { $sModule = $oModule->GetName(); $sModuleRootDir = $oModule->GetRootDir(); @@ -378,7 +406,7 @@ class RunTimeEnvironment { // Loop while new modules are added... $bModuleAdded = false; - foreach($aModules as $foo => $oModule) + foreach($aModules as $oModule) { if (!array_key_exists($oModule->GetName(), $aRet) && $oModule->IsAutoSelect()) { @@ -437,7 +465,6 @@ class RunTimeEnvironment // in case there is no delta the operation will be done after the end of the loop $oFactory->SaveToFile(APPROOT.'data/datamodel-'.$this->sTargetEnv.'.xml'); } - $sModule = $oModule->GetName(); $oFactory->LoadModule($oModule); if ($oFactory->HasLoadErrors()) { @@ -475,8 +502,8 @@ class RunTimeEnvironment $oMFCompiler->Compile($sTargetDir, null, $bUseSymLinks); $sCacheDir = APPROOT.'data/cache-'.$this->sTargetEnv; - Setuputils::builddir($sCacheDir); - Setuputils::tidydir($sCacheDir); + SetupUtils::builddir($sCacheDir); + SetupUtils::tidydir($sCacheDir); require_once(APPROOT.'/core/dict.class.inc.php'); MetaModel::ResetCache(md5(APPROOT).'-'.$this->sTargetEnv); @@ -627,12 +654,19 @@ class RunTimeEnvironment $oConfig->Set('access_mode', $iPrevAccessMode); } - public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModules, $sModulesRelativePath, $sShortComment = null) + public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sModulesRelativePath, $sShortComment = null) { // Have it work fine even if the DB has been set in read-only mode for the users $iPrevAccessMode = $oConfig->Get('access_mode'); $oConfig->Set('access_mode', ACCESS_FULL); + if (CMDBSource::DBName() == '') + { + // In case this has not yet been done + CMDBSource::Init($oConfig->GetDBHost(), $oConfig->GetDBUser(), $oConfig->GetDBPwd(), $oConfig->GetDBName()); + CMDBSource::SetCharacterSet($oConfig->GetDBCharacterSet(), $oConfig->GetDBCollation()); + } + if ($sShortComment === null) { $sShortComment = 'Done by the setup program'; @@ -662,10 +696,11 @@ class RunTimeEnvironment $iMainItopRecord = $oInstallRec->DBInsertNoReload(); - // Record installed modules + // Record installed modules and extensions // + $aAvailableExtensions = array(); $aAvailableModules = $this->AnalyzeInstallation($oConfig, APPROOT.$sModulesRelativePath); - foreach($aSelectedModules as $sModuleId) + foreach($aSelectedModuleCodes as $sModuleId) { $aModuleData = $aAvailableModules[$sModuleId]; $sName = $sModuleId; @@ -702,6 +737,30 @@ class RunTimeEnvironment $oInstallRec->Set('installed', $iInstallationTime); $oInstallRec->DBInsertNoReload(); } + + if ($this->oExtensionsMap) + { + // Mark as chosen the selected extensions code passed to us + // Note: some other extensions may already be marked as chosen + foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) + { + if (in_array($oExtension->sCode, $aSelectedExtensionCodes)) + { + $this->oExtensionsMap->MarkAsChosen($oExtension->sCode); + } + } + + foreach($this->oExtensionsMap->GetChoices() as $oExtension) + { + $oInstallRec = new ExtensionInstallation(); + $oInstallRec->Set('code', $oExtension->sCode); + $oInstallRec->Set('label', $oExtension->sLabel); + $oInstallRec->Set('version', $oExtension->sVersion); + $oInstallRec->Set('source', $oExtension->sSource); + $oInstallRec->Set('installed', $iInstallationTime); + $oInstallRec->DBInsertNoReload(); + } + } // Restore the previous access mode $oConfig->Set('access_mode', $iPrevAccessMode); diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php index b15984921..63e17383b 100644 --- a/setup/wizardsteps.class.inc.php +++ b/setup/wizardsteps.class.inc.php @@ -27,6 +27,7 @@ require_once(APPROOT.'setup/parameters.class.inc.php'); 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'); /** * First step of the iTop Installation Wizard: Welcome screen @@ -1173,6 +1174,42 @@ class WizStepModulesChoice extends WizardStep { static protected $SEP = '_'; protected $bUpgrade = false; + + /** + * + * @var iTopExtensionsMap + */ + protected $oExtensionsMap; + + /** + * Whether we were able to load the choices from the database or not + * @var bool + */ + protected $bChoicesFromDatabase; + + public function __construct(WizardController $oWizard, $sCurrentState) + { + parent::__construct($oWizard, $sCurrentState); + $this->bChoicesFromDatabase = false; + $this->oExtensionsMap = new iTopExtensionsMap(); + $sPreviousSourceDir = $this->oWizard->GetParameter('previous_version_dir', ''); + $sConfigPath = null; + if (($sPreviousSourceDir !== '') && is_readable($sPreviousSourceDir.'/conf/production/config-itop.php')) + { + $sConfigPath = $sPreviousSourceDir.'/conf/production/config-itop.php'; + } + else if (is_readable(utils::GetConfigFilePath('production'))) + { + $sConfigPath = utils::GetConfigFilePath('production'); + } + if ($sConfigPath !== null) + { + $oConfig = new Config($sConfigPath); + $this->bChoicesFromDatabase = $this->oExtensionsMap->LoadChoicesFromDatabase($oConfig); + } + //echo '
Default: '.($this->bChoicesFromDatabase ? 'DB' : 'Guess').'
'; + } + public function GetTitle() { $aStepInfo = $this->GetStepInfo(); @@ -1210,11 +1247,12 @@ class WizStepModulesChoice extends WizardStep { // Exiting this step of the wizard, let's convert the selection into a list of modules $aModules = array(); + $aExtensions = array(); $sDisplayChoices = '
    '; for($i = 0; $i <= $index; $i++) { $aStepInfo = $this->GetStepInfo($i); - $sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedChoices[$i], $aModules); + $sDisplayChoices .= $this->GetSelectedModules($aStepInfo, $aSelectedChoices[$i], $aModules, '', '', $aExtensions); } $sDisplayChoices .= '
'; if (class_exists('CreateITILProfilesInstaller')) @@ -1222,6 +1260,7 @@ class WizStepModulesChoice extends WizardStep $this->oWizard->SetParameter('old_addon', true); } $this->oWizard->SetParameter('selected_modules', json_encode(array_keys($aModules))); + $this->oWizard->SetParameter('selected_extensions', json_encode($aExtensions)); $this->oWizard->SetParameter('display_choices', $sDisplayChoices); return array('class' => 'WizStepSummary', 'state' => ''); } @@ -1277,7 +1316,7 @@ class WizStepModulesChoice extends WizardStep // Build the default choices $aDefaults = array(); $aModules = SetupUtils::AnalyzeInstallation($this->oWizard); - $this->GetDefaults($aStepInfo, $aDefaults, $aModules); + $aDefaults = $this->GetDefaults($aStepInfo, $aModules); //echo "
aStepInfo:\n ".print_r($aStepInfo, true)."
"; //echo "
aDefaults:\n ".print_r($aDefaults, true)."
"; @@ -1351,8 +1390,82 @@ EOF EOF ); } + + protected function GetDefaults($aInfo, $aModules, $sParentId = '') + { + $aDefaults = array(); + if (!$this->bChoicesFromDatabase) + { + $this->GuessDefaultsFromModules($aInfo, $aDefaults, $aModules, $sParentId); + } + else + { + $this->GetDefaultsFromDatabase($aInfo, $aDefaults, $sParentId); + } + return $aDefaults; + } + + protected function GetDefaultsFromDatabase($aInfo, &$aDefaults, $sParentId) + { + $aOptions = isset($aInfo['options']) ? $aInfo['options'] : array(); + foreach($aOptions as $index => $aChoice) + { + $sChoiceId = $sParentId.self::$SEP.$index; + if ($this->bUpgrade) + { + if ($this->oExtensionsMap->IsMarkedAsChosen($aChoice['extension_code'])) + { + $aDefaults[$sChoiceId] = $sChoiceId; + } + } + else if (isset($aChoice['default']) && $aChoice['default']) + { + $aDefaults[$sChoiceId] = $sChoiceId; + } + // Recurse for sub_options (if any) + if (isset($aChoice['sub_options'])) + { + $this->GetDefaultsFromDatabase($aChoice['sub_options'], $aDefaults, $sChoiceId); + } + } - protected function GetDefaults($aInfo, &$aDefaults, $aModules, $sParentId = '') + $aAlternatives = isset($aInfo['alternatives']) ? $aInfo['alternatives'] : array(); + $sChoiceName = null; + foreach($aAlternatives as $index => $aChoice) + { + $sChoiceId = $sParentId.self::$SEP.$index; + if ($sChoiceName == null) + { + $sChoiceName = $sChoiceId; // All radios share the same name + } + if ($this->bUpgrade) + { + if ($this->oExtensionsMap->IsMarkedAsChosen($aChoice['extension_code'])) + { + $aDefaults[$sChoiceName] = $sChoiceId; + } + } + else if (isset($aChoice['default']) && $aChoice['default']) + { + $aDefaults[$sChoiceName] = $sChoiceId; + } + // Recurse for sub_options (if any) + if (isset($aChoice['sub_options'])) + { + $this->GetDefaultsFromDatabase($aChoice['sub_options'], $aDefaults, $sChoiceId); + } + } + } + + /** + * Try to guess the user choices based on the current list of installed modules... + * @param array $aInfo + * @param array $aDefaults + * @param array $aModules + * @param string $sParentId + * @return array + */ + protected function GuessDefaultsFromModules($aInfo, &$aDefaults, $aModules, $sParentId = '') { $aRetScore = array(); $aScores = array(); @@ -1387,7 +1500,7 @@ EOF if (isset($aChoice['sub_options'])) { - $aScores[$sChoiceId] = array_merge($aScores[$sChoiceId], $this->GetDefaults($aChoice['sub_options'], $aDefaults, $sChoiceId)); + $aScores[$sChoiceId] = array_merge($aScores[$sChoiceId], $this->GuessDefaultsFromModules($aChoice['sub_options'], $aDefaults, $sChoiceId)); } $index++; } @@ -1409,7 +1522,11 @@ EOF } if (isset($aChoice['sub_options'])) { - $aScores[$sChoiceId] = $this->GetDefaults($aChoice['sub_options'], $aDefaults, $aModules, $sChoiceId); + // By default (i.e. install-mode), sub options can only be checked if the parent option is checked by default + if ($this->bUpgrade || (isset($aChoice['default']) && $aChoice['default'])) + { + $aScores[$sChoiceId] = $this->GuessDefaultsFromModules($aChoice['sub_options'], $aDefaults, $aModules, $sChoiceId); + } } $index++; } @@ -1474,9 +1591,9 @@ EOF * @param hash $aSelectedChoices List of selected choices array('name' => 'selected_value_id') * @param hash $aModules Return parameter: List of selected modules array('module_id' => true) * @param string $sParentId Used for recursion - * @return void + * @return string A text representation of what will be installed */ - protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sParentId = '', $sDisplayChoices = '') + protected function GetSelectedModules($aInfo, $aSelectedChoices, &$aModules, $sParentId = '', $sDisplayChoices = '', &$aSelectedExtensions = null) { if ($sParentId == '') { @@ -1508,11 +1625,16 @@ EOF $aModules[$sModuleId] = true; // store the Id of the selected module } } + $sChoiceType = isset($aChoice['type']) ? $aChoice['type'] : 'wizard_option'; + if ($aSelectedExtensions !== null) + { + $aSelectedExtensions[] = $aChoice['extension_code']; + } // Recurse only for selected choices if (isset($aChoice['sub_options'])) { $sDisplayChoices .= '
    '; - $sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices); + $sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions); $sDisplayChoices .= '
'; } $sDisplayChoices .= ''; @@ -1533,6 +1655,10 @@ EOF (isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) ) { $sDisplayChoices .= '
  • '.$aChoice['title'].'
  • '; + if ($aSelectedExtensions !== null) + { + $aSelectedExtensions[] = $aChoice['extension_code']; + } if (isset($aChoice['modules'])) { foreach($aChoice['modules'] as $sModuleId) @@ -1544,7 +1670,7 @@ EOF if (isset($aChoice['sub_options'])) { $sDisplayChoices .= '
      '; - $sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices); + $sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions); $sDisplayChoices .= '
    '; } $sDisplayChoices .= ''; @@ -1619,79 +1745,68 @@ EOF $aSteps = array(); if (@file_exists($this->GetSourceFilePath())) { + // Found an "installation.xml" file, let's us tis definition for the wizard $aParams = new XMLParameters($this->GetSourceFilePath()); $aSteps = $aParams->Get('steps', array()); - $bAddExtensionsOnly = true; - } - else - { - // No wizard configuration provided, build a standard one: - $bAddExtensionsOnly = false; - $aSteps[] = array( - 'title' => 'Modules Selection', - 'description' => '

    Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.

    ', - 'banner' => '/images/modules.png', - 'options' => array() - ); - } - - // Additional step for the extensions - $aSteps[] = array( - 'title' => 'Extensions', - 'description' => '

    Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.

    ', - 'banner' => '/images/extension.png', - 'options' => array() + + // Additional step for the "extensions" + $aStepDefinition = array( + 'title' => 'Extensions', + 'description' => '

    Select additional extensions to install. You can launch the installation again to install new extensions, but you cannot remove already installed extensions.

    ', + 'banner' => '/images/extension.png', + 'options' => array() ); - - try - { - $sDefaultAppPath = utils::GetDefaultUrlAppRoot(); - } - catch(Exception $e) - { - $sDefaultAppPath = '..'; - } - - $aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard); - foreach($aAvailableModules as $sModuleId => $aModule) - { - if ($sModuleId == ROOT_MODULE) continue; // Convention: the version number of the application (and datamodel) are stored as a module named ROOT_MODULE - - $sModuleLabel = $aModule['label']; - $sModuleHelp = $aModule['doc.more_information']; - $sMoreInfo = (!empty($aModule['doc.more_information'])) ? "more info": ''; - if (($aModule['category'] != 'authentication') && ($aModule['visible'] && !isset($aModule['auto_select']))) + + foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) { - if (($bAddExtensionsOnly) && (!$this->IsExtension($aModule))) continue; - - if ($this->IsExtension($aModule)) + if ($oExtension->sSource !== iTopExtension::SOURCE_WIZARD) { - $iStepIndex = count($aSteps) - 1; + $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), + ); } - else - { - $iStepIndex = 0; - } - $aSteps[$iStepIndex]['options'][] = array( - 'title' => $sModuleLabel, - 'description' => '', - 'more_info' => $sMoreInfo, - 'default' => true, // by default offer to install all modules - 'modules' => array($sModuleId), - 'mandatory' => ($aModule['install']['flag'] & MODULE_ACTION_MANDATORY) ? true : false, - ); } - } - - if (count($aSteps[count($aSteps) - 1]['options']) == 0) - { - // No extensions at all, remove the last step - $this->oWizard->SetParameter('additional_extensions_modules', '[]'); - array_pop($aSteps); + // 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 { - $this->oWizard->SetParameter('additional_extensions_modules', json_encode($aSteps[count($aSteps) - 1]['options'])); + // No wizard configuration provided, build a standard one with just one big list + $aStepDefinition = array( + 'title' => 'Modules Selection', + 'description' => '

    Select the modules to install. You can launch the installation again to install new modules, but you cannot remove already installed modules.

    ', + 'banner' => '/images/modules.png', + 'options' => array() + ); + foreach($this->oExtensionsMap->GetAllExtensions() as $oExtension) + { + if ($oExtension->sSource) + { + $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), + ); + } + } + $aSteps[] = $aStepDefinition; } if (array_key_exists($index, $aSteps)) @@ -1702,52 +1817,70 @@ EOF return $aStepInfo; } - protected function GetExtensionsStepInfo() + protected function GetExtensionSourceLabel($sSource) { - // let the user select from the list of modules located in the "extensions" folder + switch($sSource) + { + case iTopExtension::SOURCE_MANUAL: + $sResult = 'Extension'; + break; + + case iTopExtension::SOURCE_REMOTE: + $sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop-Hub' : 'ITSM-Designer'; + break; + + default: + $sResult = ''; + } + if ($sResult == '') + { + return ''; + } + return ''.$sResult.''; } - protected function IsExtension($aModule) - { - // root_dir is the directory containing the module, check if its parent is "extensions" - if (basename(dirname($aModule['root_dir'])) == 'extensions') - { - return true; - } - return false; - } - - protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '') + protected function DisplayOptions($oPage, $aStepInfo, $aSelectedComponents, $aDefaults, $sParentId = '', $bAllDisabled = false) { $aOptions = isset($aStepInfo['options']) ? $aStepInfo['options'] : array(); $aAlternatives = isset($aStepInfo['alternatives']) ? $aStepInfo['alternatives'] : array(); $index = 0; + + $sAllDisabled = ''; + if ($bAllDisabled) + { + $sAllDisabled = 'disabled data-disabled="disabled" '; + } foreach($aOptions as $index => $aChoice) { $sAttributes = ''; $sChoiceId = $sParentId.self::$SEP.$index; + $sDataId = 'data-id="'.htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8').'"'; + $sId = htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8'); $bIsDefault = array_key_exists($sChoiceId, $aDefaults); $bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] == $sChoiceId); $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault); + $bDisabled = false; if ($bMandatory) { - $oPage->add('
     '); + $oPage->add('
     '); + $bDisabled = true; } else if ($bSelected) { - $oPage->add('
     '); + $oPage->add('
     '); } else { - $oPage->add('
     '); + $oPage->add('
     '); } - $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId); + $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled); $oPage->add('
    '); $index++; } $sChoiceName = null; $sDisabled = ''; + $bDisabled = false; $sChoiceIdNone = null; foreach($aAlternatives as $index => $aChoice) { @@ -1758,21 +1891,32 @@ EOF } $bIsDefault = array_key_exists($sChoiceName, $aDefaults) && ($aDefaults[$sChoiceName] == $sChoiceId); $bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']) || ($this->bUpgrade && $bIsDefault); - if ($bMandatory) + if ($bMandatory || $bAllDisabled) { // One choice is mandatory, all alternatives are disabled $sDisabled = ' disabled data-disabled="disabled"'; + $bDisabled = true; } if ( (!isset($aChoice['sub_options']) || (count($aChoice['sub_options']) == 0)) && (!isset($aChoice['modules']) || (count($aChoice['modules']) == 0)) ) { $sChoiceIdNone = $sChoiceId; // the "None" / empty choice } } - + + if (!array_key_exists($sChoiceName, $aDefaults) || ($aDefaults[$sChoiceName] == $sChoiceIdNone)) + { + // The "none" choice does not disable the selection !! + $sDisabled = ''; + $bDisabled = false; + } + foreach($aAlternatives as $index => $aChoice) { $sAttributes = ''; $sChoiceId = $sParentId.self::$SEP.$index; + $sDataId = 'data-id="'.htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8').'"'; + $sId = htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8'); + if ($sChoiceName == null) { $sChoiceName = $sChoiceId; // All radios share the same name @@ -1796,22 +1940,24 @@ EOF $sAttributes = ' checked '; $sHidden = ''; } - $oPage->add('
    '.$sHidden.' '); - $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId); + $oPage->add('
    '.$sHidden.' '); + $this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled); $oPage->add('
    '); $index++; } } - protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId) + protected function DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled = false) { - $sMoreInfo = isset($aChoice['more_info']) ? $aChoice['more_info'] : ''; - $oPage->add(' '.$sMoreInfo); + $sMoreInfo = (isset($aChoice['more_info']) && ($aChoice['more_info'] != '')) ? 'More information' : ''; + $sSourceLabel = isset($aChoice['source_label']) ? $aChoice['source_label'] : ''; + $sId = htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8'); + $oPage->add(' '.$sMoreInfo); $sDescription = isset($aChoice['description']) ? htmlentities($aChoice['description'], ENT_QUOTES, 'UTF-8') : ''; $oPage->add('
    '.$sDescription.''); if (isset($aChoice['sub_options'])) { - $this->DisplayOptions($oPage, $aChoice['sub_options'], $aSelectedComponents, $aDefaults, $sChoiceId); + $this->DisplayOptions($oPage, $aChoice['sub_options'], $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled); } $oPage->add('
    '); } @@ -1946,8 +2092,6 @@ EOF $aInstallParams = $this->BuildConfig(); $sMode = $aInstallParams['mode']; - - $sPreinstallationPhase = ''; $sDestination = ITOP_APPLICATION.(($sMode == 'install') ? ' version '.ITOP_VERSION.' is about to be installed ' : ' is about to be upgraded '); $sDBDescription = ' existing database '.$aInstallParams['database']['name'].''; @@ -2081,6 +2225,7 @@ EOF { $sMode = $this->oWizard->GetParameter('install_mode', 'install'); $aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true); + $aSelectedExtensions = json_decode($this->oWizard->GetParameter('selected_extensions'), true); $sBackupDestination = ''; $sPreviousConfigurationFile = ''; $sDBName = $this->oWizard->GetParameter('db_name'); @@ -2165,6 +2310,7 @@ EOF ), 'language' => $this->oWizard->GetParameter('default_language'), 'selected_modules' => $aSelectedModules, + 'selected_extensions' => $aSelectedExtensions, 'sample_data' => ($this->oWizard->GetParameter('sample_data', '') == 'yes') ? true : false , 'old_addon' => $this->oWizard->GetParameter('old_addon', false), // whether or not to use the "old" userrights profile addon 'options' => json_decode($this->oWizard->GetParameter('misc_options', '[]'), true), @@ -2203,7 +2349,7 @@ EOF //$("#percentage").html('{$aRes['percentage-completed']} % completed
    {$aRes['next-step-label']}'); ExecuteStep('{$aRes['next-step']}'); EOF - ); + ); } else if ($aRes['status'] != ApplicationInstaller::ERROR) { @@ -2318,11 +2464,31 @@ class WizStepDone extends WizardStep } // Form goes here.. No back button since the job is done ! - $oPage->add(''); + $oPage->add('
    '); $oPage->add(""); $oPage->add(""); $oPage->add(""); $oPage->add('
    '); + + $oConfig = new Config(utils::GetConfigFilePath()); + $sIframeUrl = $oConfig->GetModuleSetting('itop-hub-connector', 'setup_url', ''); + + if ($sIframeUrl != '') + { + $sIframeUrl .= '?'; + $oPage->add(''); + + $oPage->add_script("window.addEventListener('message', function(event) { + if (event.data === 'itophub_load_completed') + { + $('#fresh_content').height($('#placeholder').height()); + $('#placeholder').hide(); + $('#fresh_content').show(); + } + }, false); + "); + } + $sForm = '
    '; $sForm .= ''; $sForm .= ''; @@ -2380,7 +2546,11 @@ class WizStepDone extends WizardStep { if (in_array('_'.$idx, $aParameters[count($aParameters)-1])) { - $aAdditionalModules[] = $aModuleInfo['modules'][0]; // Extensions "choices" are always made of one module + // Extensions "choices" can now have more than one module + foreach($aModuleInfo['modules'] as $sModuleName) + { + $aAdditionalModules[] = $sModuleName; + } } } $idx = 0; @@ -2417,7 +2587,7 @@ class WizStepDone extends WizardStep public function AsyncAction(WebPage $oPage, $sCode, $aParameters) { - // For security reasons: add the extension now so that this action can be used to read *only* .zip files from the disk... + // For security reasons: add the extension now so that this action can be used to read *only* .tar.gz files from the disk... $sBackupFile = $aParameters['backup'].'.tar.gz'; if (file_exists($sBackupFile)) {