Pārlūkot izejas kodu

Some progress on the 2.0 setup...

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2237 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 12 gadi atpakaļ
vecāks
revīzija
dd9b62c32c

+ 12 - 5
core/config.class.inc.php

@@ -532,7 +532,7 @@ class Config
 			'type' => 'string',
 			'description' => 'Source directory for the datamodel files. (which gets compiled to env-production).',
 			// examples... not used
-			'default' => 'datamodels/latest',
+			'default' => '',
 			'value' => '',
 			'source_of_value' => '',
 			'show_in_conf_sample' => true,
@@ -1420,7 +1420,7 @@ class Config
 	/**
 	 * Helper function to initialize a configuration from the page arguments
 	 */
-	public function UpdateFromParams($aParamValues, $sModulesDir = null)
+	public function UpdateFromParams($aParamValues, $sModulesDir = null, $bPreserveModuleSettings = false)
 	{
 		if (isset($aParamValues['application_path']))
 		{
@@ -1469,7 +1469,7 @@ class Config
 			// Merge the values with the ones provided by the modules
 			// Make sure when don't load the same file twice...
 	
-			$aModules = ModuleDiscovery::GetAvailableModules(APPROOT, $sModulesDir);
+			$aModules = ModuleDiscovery::GetAvailableModules(array(APPROOT.$sModulesDir));
 			foreach($aModules as $sModuleId => $aModuleInfo)
 			{
 				list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
@@ -1489,10 +1489,17 @@ class Config
 					}
 					if (isset($aModuleInfo['settings']))
 					{
+						list($sName, $sVersion) = ModuleDiscovery::GetModuleName($sModuleId);
 						foreach($aModuleInfo['settings'] as $sProperty => $value)
 						{
-							list($sName, $sVersion) = ModuleDiscovery::GetModuleName($sModuleId);
-							$this->SetModuleSetting($sName, $sProperty, $value);
+							if ($bPreserveModuleSettings && isset($this->m_aModuleSettings[$sName][$sProperty]))
+							{
+								// Do nothing keep the original value
+							}
+							else
+							{
+								$this->SetModuleSetting($sName, $sProperty, $value);
+							}
 						}
 					}
 					if (isset($aModuleInfo['installer']))

BIN
images/extension.png


+ 29 - 11
setup/ajax.dataloader.php

@@ -136,18 +136,36 @@ try
 	switch($sOperation)
 	{
 		case 'async_action':
-		require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
-		require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
+		ini_set('max_execution_time', max(240, ini_get('max_execution_time')));
+		// While running the setup it is desirable to see any error that may happen
+		ini_set('display_errors', true);
+		ini_set('display_startup_errors', true);
 		
-		$sClass = utils::ReadParam('step_class', '');
-		$sState = utils::ReadParam('step_state', '');
-		$sActionCode = utils::ReadParam('code', '');
-		$aParams = utils::ReadParam('params', array(), false, 'raw_data');
-		$oPage = new ajax_page('');
-		$oDummyController = new WizardController('');
-		$oStep = new $sClass($oDummyController, $sState);
-		$oStep->AsyncAction($oPage, $sActionCode, $aParams);
-		$oPage->output();
+		$sConfigFile = utils::GetConfigFilePath();
+		if (file_exists($sConfigFile) && !is_writable($sConfigFile))
+		{
+			$oPage->error("<b>Error:</b> the configuration file '".$sConfigFile."' already exists and cannot be overwritten.");
+			$oPage->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '<b>".realpath($sConfigFile)."</b>' can be modified by the web server.");
+			$oPage->output();
+		}
+		else
+		{
+			require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
+			require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
+			
+			$sClass = utils::ReadParam('step_class', '');
+			$sState = utils::ReadParam('step_state', '');
+			$sActionCode = utils::ReadParam('code', '');
+			$aParams = utils::ReadParam('params', array(), false, 'raw_data');
+			$oPage = new ajax_page('');
+			$oDummyController = new WizardController('');
+			if (is_subclass_of($sClass, 'WizardStep'))
+			{
+				$oStep = new $sClass($oDummyController, $sState);
+				$oStep->AsyncAction($oPage, $sActionCode, $aParams);
+			}
+			$oPage->output();
+		}
 		break;
 
 		//////////////////////////////

+ 102 - 26
setup/applicationinstaller.class.inc.php

@@ -126,7 +126,7 @@ class ApplicationInstaller
 				$aPreinstall = $this->oParams->Get('preinstall');
 				$aCopies = $aPreinstall['copies'];
 
-				// disabled - $sReport = self::DoCopy($aCopies);
+				$sReport = self::DoCopy($aCopies);
 				$sReport = "copy disabled...";
 
 				$aResult = array(
@@ -167,7 +167,8 @@ class ApplicationInstaller
 				
 				case 'compile':
 				$aSelectedModules = $this->oParams->Get('selected_modules');
-				$sSourceDir = $this->oParams->Get('source_dir', 'datamodel');
+				$sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
+				$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
 				$sTargetEnvironment = $this->oParams->Get('target_env', '');
 				if ($sTargetEnvironment == '')
 				{
@@ -190,7 +191,7 @@ class ApplicationInstaller
 					}
 				}
 						
-				self::DoCompile($aSelectedModules, $sSourceDir, $sTargetDir, $sWorkspaceDir, $bUseSymbolicLinks);
+				self::DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sWorkspaceDir, $bUseSymbolicLinks);
 				
 				$aResult = array(
 					'status' => self::OK,
@@ -215,8 +216,9 @@ class ApplicationInstaller
 				$sDBPwd = $aDBParams['pwd'];
 				$sDBName = $aDBParams['name'];
 				$sDBPrefix = $aDBParams['prefix'];
+				$bOldAddon = $this->oParams->Get('old_addon', false);
 				
-				self::DoUpdateDBSchema($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment);
+				self::DoUpdateDBSchema($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment, $bOldAddon);
 				
 				$aResult = array(
 					'status' => self::OK,
@@ -247,8 +249,12 @@ class ApplicationInstaller
 				$sAdminLanguage = $aAdminParams['language'];
 				$sLanguage = $this->oParams->Get('language');
 				$aSelectedModules = $this->oParams->Get('selected_modules', array());
+				$sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
+				$bOldAddon = $this->oParams->Get('old_addon', false);
+				$sSourceDir = $this->oParams->Get('source_dir', '');
 				
-				self::AfterDBCreate($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sAdminUser, $sAdminPwd, $sAdminLanguage, $sLanguage, $aSelectedModules, $sTargetEnvironment);
+				self::AfterDBCreate($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sAdminUser,
+									$sAdminPwd, $sAdminLanguage, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sDataModelVersion, $sSourceDir);
 				
 				$aResult = array(
 					'status' => self::OK,
@@ -277,8 +283,9 @@ class ApplicationInstaller
 				$sDBName = $aDBParams['name'];
 				$sDBPrefix = $aDBParams['prefix'];
 				$aFiles = $this->oParams->Get('files', array());
+				$bOldAddon = $this->oParams->Get('old_addon', false);
 				
-				self::DoLoadFiles($aSelectedModules, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment);
+				self::DoLoadFiles($aSelectedModules, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment, $bOldAddon);
 				
 				$aResult = array(
 					'status' => self::INFO,
@@ -306,8 +313,11 @@ class ApplicationInstaller
 				$sUrl = $this->oParams->Get('url', '');
 				$sLanguage = $this->oParams->Get('language', '');
 				$aSelectedModules = $this->oParams->Get('selected_modules', array());
+				$bOldAddon = $this->oParams->Get('old_addon', false);
+				$sSourceDir = $this->oParams->Get('source_dir', '');
+				$sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
 				
-				self::DoCreateConfig($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModules, $sTargetEnvironment);
+				self::DoCreateConfig($sMode, $sTargetDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile);
 				
 				$aResult = array(
 					'status' => self::INFO,
@@ -338,6 +348,9 @@ class ApplicationInstaller
 				'next-step-label' => '',
 				'percentage-completed' => 100,
 			);
+			
+			SetupPage::log_error('An exception occurred: '.$e->getMessage());
+			SetupPage::log("Stack trace:\n".$e->getTraceAsString());
 		}
 		return $aResult;
 	}
@@ -374,7 +387,7 @@ class ApplicationInstaller
 	}
 
 	
-	protected static function DoCompile($aSelectedModules, $sSourceDir, $sTargetDir, $sWorkspaceDir = '', $bUseSymbolicLinks = false)
+	protected static function DoCompile($aSelectedModules, $sSourceDir, $sExtensionDir, $sTargetDir, $sWorkspaceDir = '', $bUseSymbolicLinks = false)
 	{
 		SetupPage::log_info("Compiling data model.");
 
@@ -388,6 +401,7 @@ class ApplicationInstaller
 		}		
 
 		$sSourcePath = APPROOT.$sSourceDir;
+		$sExtensionsPath = APPROOT.$sExtensionDir;
 		$sTargetPath = APPROOT.$sTargetDir;
 		if (!is_dir($sSourcePath))
 		{
@@ -407,7 +421,7 @@ class ApplicationInstaller
 			}
 		}
 
-		$oFactory = new ModelFactory($sSourcePath);
+		$oFactory = new ModelFactory(array($sSourcePath, $sExtensionsPath));
 		$aModules = $oFactory->FindModules();
 
 		foreach($aModules as $foo => $oModule)
@@ -441,13 +455,24 @@ class ApplicationInstaller
 		}
 		else
 		{
-			$oMFCompiler = new MFCompiler($oFactory, $sSourcePath);
+			$oMFCompiler = new MFCompiler($oFactory);
 			$oMFCompiler->Compile($sTargetPath, null, $bUseSymbolicLinks);
 			SetupPage::log_info("Data model successfully compiled to '$sTargetPath'.");
 		}
+		
+		// Special case to patch a ugly patch in itop-config-mgmt
+		$sFileToPatch = $sTargetPath.'/itop-config-mgmt-1.0.0/model.itop-config-mgmt.php';
+		if (file_exists($sFileToPatch))
+		{
+			$sContent = file_get_contents($sFileToPatch);
+			
+			$sContent = str_replace("require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", "//\n// The line below is no longer needed in iTop 2.0 -- patched by the setup program\n// require_once(APPROOT.'modules/itop-welcome-itil/model.itop-welcome-itil.php');", $sContent);
+			
+			file_put_contents($sFileToPatch, $sContent);
+		}
 	}
 	
-	protected static function DoUpdateDBSchema($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment  = '')
+	protected static function DoUpdateDBSchema($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment  = '', $bOldAddon = false)
 	{
 		SetupPage::log_info("Update Database Schema for environment '$sTargetEnvironment'.");
 
@@ -461,7 +486,14 @@ class ApplicationInstaller
 			'db_prefix' => $sDBPrefix,
 		);
 		$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
+		if ($bOldAddon)
+		{
+			// Old version of the add-on for backward compatibility with pre-2.0 data models
+			$oConfig->SetAddons(array(
+				'user rights' => 'addons/userrights/userrightsprofile.db.class.inc.php',
+			));
+		}
+		
 		$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
 		$oProductionEnv->InitDataModel($oConfig, true);  // load data model only
 
@@ -472,7 +504,7 @@ class ApplicationInstaller
 		SetupPage::log_info("Database Schema Successfully Updated for environment '$sTargetEnvironment'.");
 	}
 	
-	protected static function AfterDBCreate($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sAdminUser, $sAdminPwd, $sAdminLanguage, $sLanguage, $aSelectedModules, $sTargetEnvironment  = '')
+	protected static function AfterDBCreate($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sAdminUser, $sAdminPwd, $sAdminLanguage, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sDataModelVersion, $sSourceDir)
 	{
 
 		SetupPage::log_info('After Database Creation');
@@ -487,18 +519,27 @@ class ApplicationInstaller
 			'db_prefix' => $sDBPrefix,
 		);
 		$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
+		if ($bOldAddon)
+		{
+			// Old version of the add-on for backward compatibility with pre-2.0 data models
+			$oConfig->SetAddons(array(
+				'user rights' => 'addons/userrights/userrightsprofile.db.class.inc.php',
+			));
+		}
+		$oConfig->Set('source_dir', $sSourceDir); // Needed by RecordInstallation below
+		
 		$oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
-		$oProductionEnv->InitDataModel($oConfig, false);  // load data model and connect to the database
+		$oProductionEnv->InitDataModel($oConfig, true);  // load data model and connect to the database
+		
 		self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously 
 		
 		// Perform here additional DB setup... profiles, etc...
 		//
-		$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), $sModulesDir);
+		$aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
 		foreach($aAvailableModules as $sModuleId => $aModule)
 		{
 			if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules) &&
-			     isset($aAvailableModules[$sModuleId]['installer']) )
+				isset($aAvailableModules[$sModuleId]['installer']) )
 			{
 				$sModuleInstallerClass = $aAvailableModules[$sModuleId]['installer'];
 				SetupPage::log_info("Calling Module Handler: $sModuleInstallerClass::AfterDatabaseCreation(oConfig, {$aModule['version_db']}, {$aModule['version_code']})");
@@ -555,7 +596,7 @@ class ApplicationInstaller
 			}
 		}
 
-		if (!$oProductionEnv->RecordInstallation($oConfig, $aSelectedModules, $sModulesDir))
+		if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, $sModulesDir))
 		{
 			throw new Exception("Failed to record the installation information");
 		}
@@ -591,7 +632,7 @@ class ApplicationInstaller
 		}
 	}
 	
-	protected static function DoLoadFiles($aSelectedModules, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment = '')
+	protected static function DoLoadFiles($aSelectedModules, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sTargetEnvironment = '', $bOldAddon = false)
 	{
 		$aParamValues = array(
 			'db_server' => $sDBServer,
@@ -604,7 +645,14 @@ class ApplicationInstaller
 		$oConfig = new Config();
 
 		$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
+		if ($bOldAddon)
+		{
+			// Old version of the add-on for backward compatibility with pre-2.0 data models
+			$oConfig->SetAddons(array(
+				'user rights' => 'addons/userrights/userrightsprofile.db.class.inc.php',
+			));
+		}
+		
 		//Load the MetaModel if needed (asynchronous mode)
 		if (!self::$bMetaModelStarted)
 		{
@@ -624,7 +672,7 @@ class ApplicationInstaller
 
 		$aFiles = array();		
 		$oProductionEnv = new RunTimeEnvironment();
-		$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $sModulesDir);
+		$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
 		foreach($aAvailableModules as $sModuleId => $aModule)
 		{
 			if (($sModuleId != ROOT_MODULE))
@@ -658,7 +706,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  = '')
+	protected static function DoCreateConfig($sMode, $sModulesDir, $sDBServer, $sDBUser, $sDBPwd, $sDBName, $sDBPrefix, $sUrl, $sLanguage, $aSelectedModules, $sTargetEnvironment, $bOldAddon, $sSourceDir, $sPreviousConfigFile)
 	{	
 		$aParamValues = array(
 			'db_server' => $sDBServer,
@@ -668,20 +716,46 @@ class ApplicationInstaller
 			'new_db_name' => $sDBName,
 			'db_prefix' => $sDBPrefix,
 			'application_path' => $sUrl,
-			'mode' => $sMode,
 			'language' => $sLanguage,
 			'selected_modules' => implode(',', $aSelectedModules), 
 		);
 		
-		$oConfig = new Config();
+		$bPreserveModuleSettings = false;
+		if ($sMode == 'upgrade')
+		{
+			try 
+			{
+				$oOldConfig = new Config($sPreviousConfigFile);
+				$oConfig = clone($oOldConfig);
+				$bPreserveModuleSettings = true;
+			}
+			catch(Exception $e)
+			{
+				// In case the previous configuration is corrupted... start with a blank new one
+				$oConfig = new Config();
+			}
+		}
+		else
+		{
+			$oConfig = new Config();
+		}
+		
 		
 		// Migration: force utf8_unicode_ci as the collation to make the global search
 		// NON case sensitive
 		$oConfig->SetDBCollation('utf8_unicode_ci');
 		
 		// Final config update: add the modules
-		$oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
+		$oConfig->UpdateFromParams($aParamValues, $sModulesDir, $bPreserveModuleSettings);
+		if ($bOldAddon)
+		{
+			// Old version of the add-on for backward compatibility with pre-2.0 data models
+			$oConfig->SetAddons(array(
+				'user rights' => 'addons/userrights/userrightsprofile.db.class.inc.php',
+			));
+		}
+		$oConfig->Set('source_dir', $sSourceDir);
+		
 		// Make sure the root configuration directory exists
 		if (!file_exists(APPCONF))
 		{
@@ -700,5 +774,7 @@ class ApplicationInstaller
 			
 		// try to make the final config file read-only
 		@chmod($sConfigFile, 0444); // Read-only for owner and group, nothing for others
+		// Ready to go !!
+		MetaModel::ResetCache($sTargetEnvironment);
 	}
 }

+ 9 - 10
setup/compiler.class.inc.php

@@ -27,15 +27,14 @@ class DOMFormatException extends Exception
 class MFCompiler
 {
 	protected $oFactory;
-	protected $sSourceDir;
+	protected $aSourceDirs;
 
 	protected $aRootClasses;
 	protected $aLog;
 
-	public function __construct($oModelFactory, $sSourceDir)
+	public function __construct($oModelFactory)
 	{
 		$this->oFactory = $oModelFactory;
-		$this->sSourceDir = $sSourceDir;
 
 		$this->aLog = array();
 	}
@@ -97,7 +96,7 @@ class MFCompiler
 			$sModuleVersion = $oModule->GetVersion();
 		
 			$sModuleRootDir = realpath($oModule->GetRootDir());
-			$sRelativeDir = substr($sModuleRootDir, strlen($this->sSourceDir) + 1);
+			$sRelativeDir = basename($sModuleRootDir);
 		
 			// Push the other module files
 			SetupUtils::copydir($sModuleRootDir, $sTargetDir.'/'.$sRelativeDir, $bUseSymbolicLinks);
@@ -878,11 +877,12 @@ EOF;
 			{
 				$sPHP .= "\nrequire_once('$sIncludeFile'); // Implementation of the class $sParentClass\n";
 			}
-			$sFullPath =  $this->sSourceDir.'/'.$sModuleRelativeDir.'/'.$sIncludeFile;
-			if (!file_exists($sFullPath))
-			{
-				throw new Exception("Failed to process class '".$oClass->getAttribute('id')."', from '$sModuleRelativeDir'. The required include file: '$sFullPath' does not exist.");
-			}
+//TODO fix this !!!
+//			$sFullPath =  $this->sSourceDir.'/'.$sModuleRelativeDir.'/'.$sIncludeFile;
+//			if (!file_exists($sFullPath))
+//			{
+//				throw new Exception("Failed to process class '".$oClass->getAttribute('id')."', from '$sModuleRelativeDir'. The required include file: '$sFullPath' does not exist.");
+//			}
 		}
 		else
 		{
@@ -1151,7 +1151,6 @@ EOF;
 
 		$sPHP =
 <<<EOF
-
 //
 // List of constant profiles
 // - used by the class URP_Profiles at setup (create/update/delete records)

+ 7 - 0
setup/index-new.php

@@ -30,5 +30,12 @@ require_once(APPROOT.'/setup/setuppage.class.inc.php');
 require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
 require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
 
+clearstatcache(); // Make sure we know what we are doing !
+// Set a long (at least 4 minutes) execution time for the setup to avoid timeouts during this phase
+ini_set('max_execution_time', max(240, ini_get('max_execution_time')));
+// While running the setup it is desirable to see any error that may happen
+ini_set('display_errors', true);
+ini_set('display_startup_errors', true);
+
 $oWizard = new WizardController('WizStepWelcome');
 $oWizard->Run();

+ 1 - 1
setup/modelfactory.class.inc.php

@@ -1076,7 +1076,7 @@ EOF
 	 */
 	public function FindModules($sSubDirectory = '')
 	{
-		$aAvailableModules = ModuleDiscovery::GetAvailableModules($this->sRootDir, $sSubDirectory);
+		$aAvailableModules = ModuleDiscovery::GetAvailableModules($this->sRootDir, array($sSubDirectory));
 		$aResult = array();
 		foreach($aAvailableModules as $sId => $aModule)
 		{

+ 82 - 29
setup/modulediscovery.class.inc.php

@@ -30,11 +30,8 @@ class ModuleDiscovery
 	);
 	
 
-	// Cache the results and the source directory
-	// Note that, as class can be declared within the module files, they cannot be loaded twice.
-	// Then the following assumption is made: within the same execution page, the module
-	// discovery CANNOT be executed on several different paths
-	protected static $m_sModulesRoot = null;
+	// Cache the results and the source directories
+	protected static $m_aSearchDirs = null;
 	protected static $m_aModules = array();
 
 	// All the entries below are list of file paths relative to the module directory
@@ -164,45 +161,44 @@ class ModuleDiscovery
 	/**
 	 * Search (on the disk) for all defined iTop modules, load them and returns the list (as an array)
 	 * of the possible iTop modules to install
-	 * @param sRootDir Application root directory
-	 * @param sSearchDir Directory to search (relative to root dir)
+	 * @param aSearchDirs Array of directories to search (absolute paths)
 	 * @return Hash A big array moduleID => ModuleData
 	 */
-	public static function GetAvailableModules($sRootDir, $sSearchDir, $oP = null)
+	public static function GetAvailableModules($aSearchDirs, $oP = null)
 	{
-		$sLookupDir = realpath($sRootDir.'/'.$sSearchDir);
-		
-		if (self::$m_sModulesRoot != $sLookupDir)
+		if (self::$m_aSearchDirs != $aSearchDirs)
 		{
 			self::ResetCache();
 		}
 		
-		if (is_null(self::$m_sModulesRoot))
+		if (is_null(self::$m_aSearchDirs))
 		{
-			// First call
-			//
-			if ($sLookupDir == '')
+			self::$m_aSearchDirs = $aSearchDirs;
+			
+			// Not in cache, let's scan the disk
+			foreach($aSearchDirs as $sSearchDir)
 			{
-				throw new Exception("Invalid directory '$sRootDir/$sSearchDir'");
+				$sLookupDir = realpath($sSearchDir);			
+				if ($sLookupDir == '')
+				{
+					throw new Exception("Invalid directory '$sSearchDir'");
+				}
+	
+				clearstatcache();
+				self::ListModuleFiles(basename($sSearchDir), dirname($sSearchDir));
 			}
-			self::$m_sModulesRoot = $sLookupDir;
-
-			clearstatcache();
-			self::ListModuleFiles($sSearchDir, $sRootDir);
 			return self::GetModules($oP);
 		}
 		else
 		{
 			// Reuse the previous results
-			//
 			return self::GetModules($oP);
 		}
 	}
 	
 	public static function ResetCache()
 	{
-		self::$m_sModulesRoot = null;
-		self::$m_sModulesRoot = null;
+		self::$m_aSearchDirs = null;
 		self::$m_aModules = array();
 	}
 
@@ -233,6 +229,8 @@ class ModuleDiscovery
 	 */    
 	protected static function ListModuleFiles($sRelDir, $sRootDir)
 	{
+		static $iDummyClassIndex = 0;
+		static $aDefinedClasses = array();
 		$sDirectory = $sRootDir.'/'.$sRelDir;
 		
 		if ($hDir = opendir($sDirectory))
@@ -253,15 +251,35 @@ class ModuleDiscovery
 					self::SetModulePath($sRelDir);
 					try
 					{
-						//echo "<p>Loading: $sDirectory/$sFile...</p>\n";
-						//SetupPage::log_info("Discovered module $sFile");
-						require($sDirectory.'/'.$sFile); // WARNING require_once will NOT work IIF doing an unattended installation WITH symbolic links
-														 // since datamodel/xxx/module.xxx.php and env-production/xxx/module.xxx.php are actually the same file (= inode)
+						$sModuleFileContents = file_get_contents($sDirectory.'/'.$sFile);
+						$sModuleFileContents = str_replace(array('<?php', '?>'), '', $sModuleFileContents);
+						$sModuleFileContents = str_replace('__FILE__', "'".addslashes($sDirectory.'/'.$sFile)."'", $sModuleFileContents);
+						preg_match_all('/class ([A-Za-z0-9_]+) extends ([A-Za-z0-9_]+)/', $sModuleFileContents, $aMatches);
+						//print_r($aMatches);
+						$idx = 0;
+						foreach($aMatches[1] as $sClassName)
+						{
+							if (class_exists($sClassName))
+							{
+								// rename the class inside the code to prevent a "duplicate class" declaration
+								// and change its parent class as well so that nobody will find it and try to execute it
+								$sModuleFileContents = str_replace($sClassName.' extends '.$aMatches[2][$idx], $sClassName.'_'.($iDummyClassIndex++).' extends DummyHandler', $sModuleFileContents);
+							}
+							$idx++;
+						}
+						$bRet = eval($sModuleFileContents);
+						
+						if ($bRet === false)
+						{
+							SetupPage::log_warning("Eval of $sRelDir/$sFile returned false");
+						}
+						
 						//echo "<p>Done.</p>\n";
 					}
 					catch(Exception $e)
 					{
 						// Continue...
+						SetupPage::log_warning("Eval of $sRelDir/$sFile caused an exception: ".$e->getMessage());
 					}
 				}
 			}
@@ -279,6 +297,41 @@ class ModuleDiscovery
  *  the declaration of a module invokes SetupWebPage::AddModule()
  *  whereas the new form is ModuleDiscovery::AddModule()
  */  
-class SetupWebPage extends ModuleDiscovery{}
+class SetupWebPage extends ModuleDiscovery
+{
+	// For backward compatibility with old modules...
+	public static function log_error($sText)
+	{
+		SetupPage::log_error($sText);
+	}
 
-?>
+	public static function log_warning($sText)
+	{
+		SetupPage::log_warning($sText);
+	}
+
+	public static function log_info($sText)
+	{
+		SetupPage::log_info($sText);
+	}
+
+	public static function log_ok($sText)
+	{
+		SetupPage::log_ok($sText);
+	}
+
+	public static function log($sText)
+	{
+		SetupPage::log($sText);
+	}	
+}
+		
+/** Ugly patch !!!
+ * In order to be able to analyse / load several times
+ * the same module file, we rename the class (to avoid duplicate class definitions)
+ * and we make the class extends the dummy class below in order to "deactivate" completely
+ * the class (in case some piece of code enumerate the classes derived from a well known class)
+ * Note that this will not work if someone enumerates the classes that implement a given interface
+ */
+class DummyHandler {
+}

+ 78 - 11
setup/runtimeenv.class.inc.php

@@ -32,6 +32,7 @@ define ('MODULE_ACTION_OPTIONAL', 1);
 define ('MODULE_ACTION_MANDATORY', 2);
 define ('MODULE_ACTION_IMPOSSIBLE', 3);
 define ('ROOT_MODULE', '_Root_'); // Convention to store IN MEMORY the name/version of the root module i.e. application
+define ('DATAMODEL_MODULE', 'datamodel'); // Convention to store the version of the datamodel
 
 class RunTimeEnvironment
 {
@@ -91,7 +92,8 @@ class RunTimeEnvironment
 	/**
 	 * Analyzes the current installation and the possibilities
 	 * 
-	 * @param $oConfig Config Defines the target environment (DB)
+	 * @param Config $oConfig Defines the target environment (DB)
+	 * @param mixed $modulesPath Either a single string or an array of absolute paths
 	 * @return hash Array with the following format:
 	 * array =>
 	 *     'iTop' => array(
@@ -115,7 +117,7 @@ class RunTimeEnvironment
 	 *     )
 	 * )
 	 */     
-	public function AnalyzeInstallation($oConfig, $sModulesRelativePath)
+	public function AnalyzeInstallation($oConfig, $modulesPath)
 	{
 		$aRes = array(
 			ROOT_MODULE => array(
@@ -126,7 +128,8 @@ class RunTimeEnvironment
 			)
 		);
 	
-		$aModules = ModuleDiscovery::GetAvailableModules(APPROOT, $sModulesRelativePath);
+		$aDirs = is_array($modulesPath) ? $modulesPath : array($modulesPath);
+		$aModules = ModuleDiscovery::GetAvailableModules($aDirs);
 		foreach($aModules as $sModuleId => $aModuleInfo)
 		{
 			list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
@@ -137,7 +140,8 @@ class RunTimeEnvironment
 			if ($sModuleVersion == '')
 			{
 				// The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not)
-				throw new Exception("Missing version for the module: '$sModuleId'");
+				//throw new Exception("Missing version for the module: '$sModuleId'");
+				$sModuleVersion  = '1.0.0';
 			}
 	
 			$sModuleAppVersion = $aModuleInfo['itop_version'];
@@ -225,7 +229,7 @@ class RunTimeEnvironment
 		//
 		foreach ($aInstallByModule as $sModuleName => $aModuleDB)
 		{
-				if ($sModuleName == ROOT_MODULE) continue; // Skip the main module
+			if ($sModuleName == ROOT_MODULE) continue; // Skip the main module
 				
 			if (!array_key_exists($sModuleName, $aRes))
 			{
@@ -284,7 +288,7 @@ class RunTimeEnvironment
 		//
 		$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
 		$oSourceEnv = new RunTimeEnvironment($sSourceEnv);
-		$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $sSourceDir);
+		$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $sSourceDir); //TODO: use an absolute PATH
 
 		// Do load the required modules
 		//
@@ -419,8 +423,19 @@ class RunTimeEnvironment
 		return true;
 	}
 	
-	public function RecordInstallation(Config $oConfig, $aSelectedModules, $sModulesRelativePath)
+	public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModules, $sModulesRelativePath)
 	{
+		// Record datamodel version
+		$aData = array(
+			'source_dir' => $oConfig->Get('source_dir'),
+		);
+		$oInstallRec = new ModuleInstallation();
+		$oInstallRec->Set('name', DATAMODEL_MODULE);
+		$oInstallRec->Set('version', $sDataModelVersion);
+		$oInstallRec->Set('comment', json_encode($aData, true));
+		$oInstallRec->Set('parent_id', 0); // root module
+		$iMainItopRecord = $oInstallRec->DBInsertNoReload();
+		
 		// Record main installation
 		$oInstallRec = new ModuleInstallation();
 		$oInstallRec->Set('name', ITOP_APPLICATION);
@@ -429,9 +444,10 @@ class RunTimeEnvironment
 		$oInstallRec->Set('parent_id', 0); // root module
 		$iMainItopRecord = $oInstallRec->DBInsertNoReload();
 	
+		
 		// Record installed modules
 		//
-		$aAvailableModules = $this->AnalyzeInstallation($oConfig, $sModulesRelativePath);
+		$aAvailableModules = $this->AnalyzeInstallation($oConfig, APPROOT.$sModulesRelativePath);
 		foreach($aSelectedModules as $sModuleId)
 		{
 			$aModuleData = $aAvailableModules[$sModuleId];
@@ -471,6 +487,60 @@ class RunTimeEnvironment
 		// Database is created, installation has been tracked into it
 		return true;	
 	}
+	
+	public function GetApplicationVersion(Config $oConfig)
+	{
+		$aResult = false;
+		try
+		{
+			require_once(APPROOT.'/core/cmdbsource.class.inc.php');
+			CMDBSource::Init($oConfig->GetDBHost(), $oConfig->GetDBUser(), $oConfig->GetDBPwd(), $oConfig->GetDBName());
+			$aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->GetDBSubname()."priv_module_install");
+		}
+		catch (MySQLException $e)
+		{
+			// No database or erroneous information
+			return false;
+		}
+	
+		// Scan the list of installed modules to get the version of the 'ROOT' module which holds the main application version
+		foreach ($aSelectInstall as $aInstall)
+		{
+			$sModuleVersion = $aInstall['version'];
+			if ($sModuleVersion == '')
+			{
+				// Though the version cannot be empty in iTop 2.0, it used to be possible
+				// therefore we have to put something here or the module will not be considered
+				// as being installed
+				$sModuleVersion = '0.0.0';
+			}
+	
+			if ($aInstall['parent_id'] == 0)
+			{
+				if ($aInstall['name'] == DATAMODEL_MODULE)
+				{
+					$aResult['datamodel_version'] = $sModuleVersion;
+					$aComments = json_decode($aInstall['comment'], true);
+					if (is_array($aComments))
+					{
+						$aResult = array_merge($aResult, $aComments);
+					}
+				}
+				else
+				{
+					$aResult['product_name'] = $aInstall['name'];
+					$aResult['product_version'] = $sModuleVersion;
+				}
+			}
+		}
+		if (!array_key_exists('datamodel_version', $aResult))
+		{
+			// Versions prior to 2.0 did not record the version of the datamodel
+			// so assume that the datamodel version is equal to the application version
+			$aResult['datamodel_version'] = $aResult['product_version'];
+		}
+		return $aResult;	
+	}
 
 	public static function MakeDirSafe($sDir)
 	{
@@ -501,6 +571,3 @@ class RunTimeEnvironment
 		SetupPage::log_ok($sText);
 	}
 } // End of class
-
-
-?>

+ 0 - 2
setup/setuppage.class.inc.php

@@ -304,5 +304,3 @@ h3.clickable.open {
 		}
 	}
 } // End of class
-
-?>

+ 148 - 5
setup/setuputils.class.inc.php

@@ -657,7 +657,7 @@ function DoCheckDBConnection()
 		'db_pwd': $("#db_pwd").val(),
 		'db_name': $("#db_name").val()
 	}
-	if (oXHRCheckDB !== null)
+	if ((oXHRCheckDB != null) && (oXHRCheckDB != undefined))
 	{
 		oXHRCheckDB.abort();
 		oXHRCheckDB = null;
@@ -937,6 +937,21 @@ EOF
 	{
 		require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
 		$oConfig = new Config();
+		$sSourceDir = $oWizard->GetParameter('source_dir', '');
+		
+		if (strpos($sSourceDir, APPROOT) !== false)
+		{
+			$sRelativeSourceDir = str_replace(APPROOT, '', $sSourceDir);
+		}
+		else if (strpos($sSourceDir, $oWizard->GetParameter('previous_version_dir')) !== false)
+		{
+			$sRelativeSourceDir = str_replace($oWizard->GetParameter('previous_version_dir'), '', $sSourceDir);
+		}
+		else
+		{
+			throw(new Exception('Internal error: AnalyzeInstallation: source_dir is neither under APPROOT nor under previous_installation_dir ???'));
+		}
+		
 		
 		$aParamValues = array(
 			'db_server' => $oWizard->GetParameter('db_server', ''),
@@ -944,16 +959,43 @@ EOF
 			'db_pwd' => $oWizard->GetParameter('db_pwd', ''),
 			'db_name' => $oWizard->GetParameter('db_name', ''),
 			'db_prefix' => $oWizard->GetParameter('db_prefix', ''),
-			'source_dir' => APPROOT.'datamodel',
+			'source_dir' => $sRelativeSourceDir,
 		);
 		$oConfig->UpdateFromParams($aParamValues, 'datamodel');
+		$aDirsToScan = array($sSourceDir);
 
+		if (is_dir($sSourceDir.'/extensions'))
+		{
+			$aDirsToScan[] = $sSourceDir.'/extensions';
+		}
+		if (is_dir($oWizard->GetParameter('copy_extensions_from')))
+		{
+			$aDirsToScan[] = $oWizard->GetParameter('copy_extensions_from');
+		}
 		$oProductionEnv = new RunTimeEnvironment();
-		$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, 'datamodel');
+		$aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, $aDirsToScan);
 
 		return $aAvailableModules;
 	}
 
+	public static function GetApplicationVersion($oWizard)
+	{
+		require_once(APPROOT.'/setup/moduleinstaller.class.inc.php');
+		$oConfig = new Config();
+		
+		$aParamValues = array(
+			'db_server' => $oWizard->GetParameter('db_server', ''),
+			'db_user' => $oWizard->GetParameter('db_user', ''),
+			'db_pwd' => $oWizard->GetParameter('db_pwd', ''),
+			'db_name' => $oWizard->GetParameter('db_name', ''),
+			'db_prefix' => $oWizard->GetParameter('db_prefix', ''),
+			'source_dir' => '',
+		);
+		$oConfig->UpdateFromParams($aParamValues, 'datamodel');
+
+		$oProductionEnv = new RunTimeEnvironment();
+		return $oProductionEnv->GetApplicationVersion($oConfig);
+	}
 	/**
 	 * Checks if the content of a directory matches the given manifest
 	 * @param string $sBaseDir Path to the root directory of iTop
@@ -964,6 +1006,7 @@ EOF
 	 */
 	public static function CheckDirAgainstManifest($sBaseDir, $sSourceDir, $aManifest, $aExcludeNames = array('.svn'), $aResult = null)
 	{
+//echo "CheckDirAgainstManifest($sBaseDir, $sSourceDir ...)\n"; 
 		if ($aResult === null)
 		{
 			$aResult = array('added' => array(), 'removed' => array(), 'modified' => array());
@@ -990,10 +1033,13 @@ EOF
 			}
 		}
 
+//echo "The manifest contains ".count($aDirManifest)." files for the directory '$sSourceDir' (and below)\n"; 
+		
 		// Read the content of the directory
 		foreach(glob($sBaseDir.'/'.$sSourceDir .'/*') as $sFilePath)
 		{
 			$sFile = basename($sFilePath);
+//echo "Checking $sFile ($sFilePath)\n"; 
 			
 			if (in_array(basename($sFile), $aExcludeNames)) continue;
 						
@@ -1023,6 +1069,7 @@ EOF
 						if ($sMD5 != $aDirManifest[$sFile]['md5'])
 						{
 							$aResult['modified'][$sSourceDir.'/'.$sFile] = 'Content modified (MD5 checksums differ).';
+//echo $sSourceDir.'/'.$sFile." modified ($sMD5 == {$aDirManifest[$sFile]['md5']})\n";
 						}
 //else
 //{
@@ -1051,9 +1098,10 @@ EOF
 			$aManifest[] = array('path' => (string)$oFileInfo->path, 'size' => (int)$oFileInfo->size, 'md5' => (string)$oFileInfo->md5);
 		}
 		
+		$sBaseDir = preg_replace('|modules/?$|', '', $sBaseDir);
 		$aResults = self::CheckDirAgainstManifest($sBaseDir, 'modules', $aManifest);
 		
-//		echo "<pre>Comparison of ".dirname($sBaseDir)."/modules:\n".print_r($aResults, true)."</pre>";
+//		echo "<pre>Comparison of ".dirname($sBaseDir)."/modules against $sManifestFile:\n".print_r($aResults, true)."</pre>";
 		return $aResults;
 	}
 	
@@ -1084,7 +1132,7 @@ EOF
 		$aResults = array('added' => array(), 'removed' => array(), 'modified' => array());
 		foreach(array('addons', 'core', 'dictionaries', 'js', 'application', 'css', 'pages', 'synchro', 'webservices') as $sDir)
 		{
-			$aTmp = self::CheckDirAgainstManifest($sBaseDir, 'portal', $aManifest);
+			$aTmp = self::CheckDirAgainstManifest($sBaseDir, $sDir, $aManifest);
 			$aResults['added'] = array_merge($aResults['added'], $aTmp['added']);
 			$aResults['modified'] = array_merge($aResults['modified'], $aTmp['modified']);
 			$aResults['removed'] = array_merge($aResults['removed'], $aTmp['removed']);
@@ -1094,4 +1142,99 @@ EOF
 		return $aResults;
 	}
 	
+	public static function CheckVersion($sInstalledVersion, $sSourceDir)
+	{
+		$sManifestFilePath = self::GetVersionManifest($sInstalledVersion);
+		if ($sSourceDir != '')
+		{
+			if (file_exists($sManifestFilePath))
+			{
+				$aDMchanges = self::CheckDataModelFiles($sManifestFilePath, $sSourceDir);
+				//$aPortalChanges = self::CheckPortalFiles($sManifestFilePath, $sSourceDir);
+				//$aCodeChanges = self::CheckApplicationFiles($sManifestFilePath, $sSourceDir);
+				
+				//echo("Changes detected compared to $sInstalledVersion:<br/>DataModel:<br/><pre>".print_r($aDMchanges, true)."</pre>");
+				//echo("Changes detected compared to $sInstalledVersion:<br/>DataModel:<br/><pre>".print_r($aDMchanges, true)."</pre><br/>Portal:<br/><pre>".print_r($aPortalChanges, true)."</pre><br/>Code:<br/><pre>".print_r($aCodeChanges, true)."</pre>");
+				return $aDMchanges;
+			}
+			else
+			{
+				return false;
+			}
+		}
+		else
+		{
+				throw(new Exception("Cannot check version '$sInstalledVersion', no source directory provided to check the files."));
+		}
+	}
+	
+	public static function GetVersionManifest($sInstalledVersion)
+	{
+		if (preg_match('/^([0-9]+)\./', $sInstalledVersion, $aMatches))
+		{
+			return APPROOT.'datamodels/'.$aMatches[1].'.x/manifest-'.$sInstalledVersion.'.xml';
+		}
+		return false;
+	}
+	
+	public static function CheckWritableDirs($aWritableDirs)
+	{
+		$aNonWritableDirs = array();
+		foreach($aWritableDirs as $sDir)
+		{
+			$sFullPath = APPROOT.$sDir;
+			if (is_dir($sFullPath) && !is_writable($sFullPath))
+			{
+				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "The directory '".APPROOT.$sDir."' exists but is not writable for the application.");
+			}
+			else if (file_exists($sFullPath) && !is_dir($sFullPath))
+			{
+				$aNonWritableDirs[APPROOT.$sDir] = new CheckResult(CheckResult::ERROR, "A file with the same name as '".APPROOT.$sDir."' exists.");
+			}
+			else if (!is_dir($sFullPath) && !is_writable(APPROOT))
+			{
+				$aNonWritableDirs[APPROOT] = new CheckResult(CheckResult::ERROR, "The directory '".APPROOT."' is not writable, the application cannot create the directory '$sDir' inside it.");
+			}				
+		}
+		return $aNonWritableDirs;		
+	}
+	
+	public static function GetLatestDataModelDir()
+	{
+		$sBaseDir = APPROOT.'datamodels';
+		
+		$aDirs = glob($sBaseDir.'/*', GLOB_MARK | GLOB_ONLYDIR);
+		if ($aDirs !== false)
+		{
+			sort($aDirs);
+			
+			return array_pop($aDirs);
+		}
+		return false;
+	}
+	
+	public static function GetCompatibleDataModelDir($sInstalledVersion)
+	{
+		if (preg_match('/^([0-9]+)\./', $sInstalledVersion, $aMatches))
+		{
+			$sMajorVersion = $aMatches[1];
+			$sDir = APPROOT.'datamodels/'.$sMajorVersion.'.x/';
+			if (is_dir($sDir))
+			{
+				return $sDir;
+			}
+		}
+		return false;
+	}
+	
+	static public function GetDataModelVersion($sDatamodelDir)
+	{
+		$sVersionFile = $sDatamodelDir.'version.xml';
+		if (file_exists($sVersionFile))
+		{
+			$oParams = new XMLParameters($sVersionFile);
+			return $oParams->Get('version');
+		}
+		return false;
+	}
 }

+ 26 - 0
setup/wizardcontroller.class.inc.php

@@ -164,6 +164,23 @@ class WizardController
 	protected function DisplayStep(WizardStep $oStep)
 	{
 		$oPage = new SetupPage($oStep->GetTitle());
+		if ($oStep->RequiresWritableConfig())
+		{
+			$sConfigFile = utils::GetConfigFilePath();
+			if (file_exists($sConfigFile))
+			{
+				// The configuration file already exists
+				if (!is_writable($sConfigFile))
+				{
+					$oP = new SetupPage('Installation Cannot Continue');
+					$oP->add("<h2>Fatal error</h2>\n");
+					$oP->error("<b>Error:</b> the configuration file '".$sConfigFile."' already exists and cannot be overwritten.");
+					$oP->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '<b>".realpath($sConfigFile)."</b>' can be modified by the web server.");
+					$oP->output();
+					return;
+				}
+			}			
+		}
 		$oPage->add_linked_script('../setup/setup.js');
 		$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
 		$oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
@@ -433,6 +450,15 @@ abstract class WizardStep
 	}
 
 	/**
+	 * Tells whether this step of the wizard requires that the configuration file be writable
+	 * @return bool True if the wizard will possibly need to modify the configuration at some point
+	 */
+	public function RequiresWritableConfig()
+	{
+		return true;
+	}
+
+	/**
 	 * Overload this function to implement asynchronous action(s) (AJAX)
 	 * @param string $sCode The code of the action (if several actions need to be distinguished)
 	 * @param hash $aParameters The action's parameters name => value

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 564 - 225
setup/wizardsteps.class.inc.php


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels