Browse Source

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

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4815 a333f486-631f-4898-b8df-5754b55c2be0

dflaven 8 years ago
parent
commit
380917eb2c

+ 8 - 0
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;
+}

+ 8 - 0
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;
+}

+ 4 - 1
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!',

+ 4 - 1
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 !',

+ 41 - 8
pages/ajax.render.php

@@ -1231,16 +1231,30 @@ EOF
 		$oPage->add("</div>");
 
 		$oPage->add('<fieldset>');
-		$oPage->add('<legend>'.Dict::S('UI:About:Modules').'</legend>');
-		//$oPage->add(print_r($aAvailableModules, true));
-		$oPage->add("<div style=\"height: 150px; overflow: auto; font-size: smaller;\">");
+		$oPage->add('<legend>'.Dict::S('UI:About:InstallationOptions').'</legend>');
+		$oPage->add("<div style=\"max-height: 150px; overflow: auto; font-size: smaller;\">");
 		$oPage->add('<ul style="margin: 0;">');
-		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('<li>'.$aModuleData['label'].' ('.$aModuleData['version_db'].')</li>');
+			switch($oExtension->sSource)
+			{
+				case iTopExtension::SOURCE_REMOTE:
+				$sSource = ' <span class="extension-source">'.Dict::S('UI:About:RemoteExtensionSource').'</span>';
+				break;
+				
+				case iTopExtension::SOURCE_MANUAL:
+				$sSource = ' <span class="extension-source">'.Dict::S('UI:About:ManualExtensionSource').'</span>';
+				break;
+				
+				default:
+				$sSource = '';
+			}
+			$oPage->add('<li title="'.Dict::Format('UI:About:Extension_Version', $oExtension->sInstalledVersion).'">'.$oExtension->sLabel.$sSource.'</li>');
 		}
 		$oPage->add('</ul>');
 		$oPage->add("</div>");
@@ -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;

+ 12 - 13
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");
 		}

+ 458 - 0
setup/extensionsmap.class.inc.php

@@ -0,0 +1,458 @@
+<?php
+require_once(APPROOT.'/setup/parameters.class.inc.php');
+require_once(APPROOT.'/core/cmdbsource.class.inc.php');
+
+/**
+ * Basic helper class to describe an extension, with some characteristics and a list of modules
+ */
+class iTopExtension
+{
+	const SOURCE_WIZARD = 'datamodels';
+	const SOURCE_MANUAL = 'extensions';
+	const SOURCE_REMOTE = 'data';
+	
+	/**
+	 * @var string
+	 */
+	public $sCode;
+	
+	/**
+	 * @var string
+	 */
+	public $sVersion;
+	
+		/**
+	 * @var string
+	 */
+	public $sInstalledVersion;
+	
+/**
+	 * @var string
+	 */
+	public $sLabel;
+	
+	/**
+	 * @var string
+	 */
+	public $sDescription;
+	
+	/**
+	 * @var string
+	 */
+	public $sSource;
+	
+	/**
+	 * @var bool
+	 */
+	public $bMandatory;
+	
+	/**
+	 * @var string
+	 */
+	public $sMoreInfoUrl;
+	
+	/**
+	 * @var bool
+	 */
+	public $bMarkedAsChosen;
+	
+	
+	/**
+	 * @var string[]
+	 */
+	public $aModules;
+	
+	public function __construct()
+	{
+		$this->sCode = '';
+		$this->sLabel = '';
+		$this->sDescription = '';
+		$this->sSource = self::SOURCE_WIZARD;
+		$this->bMandatory = false;
+		$this->sMoreInfoUrl = '';
+		$this->bMarkedAsChosen = false;
+		$this->sVersion = ITOP_VERSION;
+		$this->sInstalledVersion = '';
+	}
+}
+
+/**
+ * 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('<?php', '?>'), '', $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;
+	}
+}

+ 7 - 1
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)

+ 11 - 3
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 <name>/<optional_operator><version>
 					// 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 {
 }
+

+ 44 - 4
setup/moduleinstallation.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -18,10 +18,10 @@
 
 
 /**
- * Persistent class Event and derived
+ * Persistent class ModuleInstallation to record the installed modules
  * Log of module installations
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -59,4 +59,44 @@ class ModuleInstallation extends cmdbAbstractObject
 	}
 }
 
-?>
+/**
+ * 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
+	}
+}
+
+

+ 68 - 9
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/<target-env>-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);

+ 272 - 102
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 '<div style="display:block;position:fixed;width:100px;height:20px;top:0;left:0;font-size:10pt;">Default: '.($this->bChoicesFromDatabase ? 'DB' : 'Guess').'</div>';
+	}
+	
 	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 = '<ul>';
 				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 .= '</ul>';
 				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 "<pre>aStepInfo:\n ".print_r($aStepInfo, true)."</pre>";
 		//echo "<pre>aDefaults:\n ".print_r($aDefaults, true)."</pre>";
 
@@ -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 .= '<ul>';
-					$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices);
+					$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions);
 					$sDisplayChoices .= '</ul>';
 				}
 				$sDisplayChoices .= '</li>';
@@ -1533,6 +1655,10 @@ EOF
 				 (isset($aSelectedChoices[$sChoiceName]) && ($aSelectedChoices[$sChoiceName] == $sChoiceId)) )
 			{
 				$sDisplayChoices .= '<li>'.$aChoice['title'].'</li>';
+				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 .= '<ul>';
-					$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices);
+					$sDisplayChoices = $this->GetSelectedModules($aChoice['sub_options'], $aSelectedChoices, $aModules, $sChoiceId, $sDisplayChoices, $aSelectedExtensions);
 					$sDisplayChoices .= '</ul>';
 				}
 				$sDisplayChoices .= '</li>';
@@ -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' => '<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/modules.png',
-				'options' => array()
-				);
-		}
-		
-		// Additional step for the extensions
-		$aSteps[] = 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/extension.png',
-			'options' => array()
+			
+			// 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/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'])) ? "<a href=\"$sDefaultAppPath{$aModule['doc.more_information']}\" target=\"_blank\">more info</a>": '';
-			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))
-				{
-					$iStepIndex = count($aSteps) - 1;
-				}
-				else
+				if ($oExtension->sSource !== iTopExtension::SOURCE_WIZARD)
 				{
-					$iStepIndex = 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),
+					);
 				}
-				$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' => '<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/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()
-	{
-		// let the user select from the list of modules located in the "extensions" folder
-	}
-	
-	protected function IsExtension($aModule)
+	protected function GetExtensionSourceLabel($sSource)
 	{
-		// root_dir is the directory containing the module, check if its parent is "extensions"
-		if (basename(dirname($aModule['root_dir'])) == 'extensions')
+		switch($sSource)
 		{
-			return true;
+			case iTopExtension::SOURCE_MANUAL:
+			$sResult = 'Extension';
+			break;
+
+			case iTopExtension::SOURCE_REMOTE:
+			$sResult = (ITOP_APPLICATION == 'iTop') ? 'iTop-Hub' : 'ITSM-Designer';
+			break;
+				
+			default:
+			$sResult = '';
 		}
-		return false;
+		if ($sResult == '')
+		{
+			return '';
+		}
+		return '<span style="display:inline-block;font-size:8pt;padding:3px;border-radius:4px;color:#fff;background-color:#1c94c4;margin-left:0.5em;margin-right:0.5em">'.$sResult.'</span>';
 	}
-		
-	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('<div class="choice"><input id="choice'.$sChoiceId.'" checked disabled data-disabled="disabled" type="checkbox"'.$sAttributes.'/><input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'">&nbsp;');
+				$oPage->add('<div class="choice" '.$sDataId.'><input id="'.$sId.'" checked disabled data-disabled="disabled" type="checkbox"'.$sAttributes.'/><input type="hidden" name="choice['.$sChoiceId.']" value="'.$sChoiceId.'">&nbsp;');
+				$bDisabled = true;
 			}
 			else if ($bSelected)
 			{
-				$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceId.']" type="checkbox" checked value="'.$sChoiceId.'"/>&nbsp;');
+				$oPage->add('<div class="choice" '.$sDataId.'><input class="wiz-choice" '.$sAllDisabled.'id="'.$sId.'" name="choice['.$sChoiceId.']" type="checkbox" checked value="'.$sChoiceId.'"/>&nbsp;');
 			}
 			else
 			{
-				$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'"/>&nbsp;');
+				$oPage->add('<div class="choice" '.$sDataId.'><input class="wiz-choice" '.$sAllDisabled.'id="'.$sId.'" name="choice['.$sChoiceId.']" type="checkbox" value="'.$sChoiceId.'"/>&nbsp;');
 			}
-			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId);
+			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled);
 			$oPage->add('</div>');
 			$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 = '<input type="hidden" name="choice['.$sChoiceName.']" value="'.$sChoiceId.'"/>';
 			}
-			$oPage->add('<div class="choice"><input class="wiz-choice" id="choice'.$sChoiceId.'" name="choice['.$sChoiceName.']" type="radio"'.$sAttributes.' value="'.$sChoiceId.'"'.$sDisabled.'/>'.$sHidden.'&nbsp;');
-			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId);
+			$oPage->add('<div class="choice" '.$sDataId.'><input class="wiz-choice" id="'.$sId.'" name="choice['.$sChoiceName.']" type="radio"'.$sAttributes.' value="'.$sChoiceId.'"'.$sDisabled.'/>'.$sHidden.'&nbsp;');
+			$this->DisplayChoice($oPage, $aChoice, $aSelectedComponents, $aDefaults, $sChoiceId, $bDisabled);
 			$oPage->add('</div>');
 			$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('<label for="choice'.$sChoiceId.'"><b>'.htmlentities($aChoice['title'], ENT_QUOTES, 'UTF-8').'</b></label> '.$sMoreInfo);
+		$sMoreInfo = (isset($aChoice['more_info']) && ($aChoice['more_info'] != '')) ? '<a target="_blank" href="'.$aChoice['more_info'].'">More information</a>' : '';
+		$sSourceLabel = isset($aChoice['source_label']) ? $aChoice['source_label'] : '';
+		$sId = htmlentities($aChoice['extension_code'], ENT_QUOTES, 'UTF-8');
+		$oPage->add('<label for="'.$sId.'"><b>'.htmlentities($aChoice['title'], ENT_QUOTES, 'UTF-8').'</b>'.$sSourceLabel.'</label> '.$sMoreInfo);
 		$sDescription = isset($aChoice['description']) ? htmlentities($aChoice['description'], ENT_QUOTES, 'UTF-8') : '';
 		$oPage->add('<div class="description">'.$sDescription.'<span id="sub_choices'.$sChoiceId.'">');
 		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('</span></div>');
 	}
@@ -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 = ' <b>existing</b> database <b>'.$aInstallParams['database']['name'].'</b>';
@@ -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<br/>{$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('<table style="width:600px;border:0;padding:0;"><tr>');
+		$oPage->add('<table id="placeholder" style="width:600px;border:0;padding:0;"><tr>');
 		$oPage->add("<td><a style=\"background:transparent;padding:0;\" title=\"Free: Register your iTop version.\" href=\"http://www.combodo.com/register?product=iTop&version=".urlencode(ITOP_VERSION." revision ".ITOP_REVISION)."\" target=\"_blank\"><img style=\"border:0\" src=\"../images/setup-register.gif\"/></td></a>");
 		$oPage->add("<td><a style=\"background:transparent;padding:0;\" title=\"Get Professional Support from Combodo\" href=\"http://www.combodo.com/itopsupport\" target=\"_blank\"><img style=\"border:0\" src=\"../images/setup-support.gif\"/></td></a>");
 		$oPage->add("<td><a style=\"background:transparent;padding:0;\" title=\"Get Professional Training from Combodo\" href=\"http://www.combodo.com/itoptraining\" target=\"_blank\"><img style=\"border:0\" src=\"../images/setup-training.gif\"/></td></a>");
 		$oPage->add('</tr></table>');
+		
+		$oConfig = new Config(utils::GetConfigFilePath());
+		$sIframeUrl = $oConfig->GetModuleSetting('itop-hub-connector', 'setup_url', '');
+		
+		if ($sIframeUrl != '')
+		{
+			$sIframeUrl .= '?';
+			$oPage->add('<iframe id="fresh_content" style="border:0; width:100%; display:none;" src="'.$sIframeUrl.'"></iframe>');
+			
+			$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 = '<form method="post" action="'.$this->oWizard->GetParameter('application_url').'pages/UI.php">';
 		$sForm .= '<input type="hidden" name="auth_user" value="'.htmlentities($this->oWizard->GetParameter('admin_user'), ENT_QUOTES, 'UTF-8').'">';
 		$sForm .= '<input type="hidden" name="auth_pwd" value="'.htmlentities($this->oWizard->GetParameter('admin_pwd'), ENT_QUOTES, 'UTF-8').'">';
@@ -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))
 		{