Loading: $sDirectory/$sFile...
\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(''), '', $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 "Done.
\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 { +} diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 556ab881e..1fc3ef4dd 100644 --- a/setup/runtimeenv.class.inc.php +++ b/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 - - -?> diff --git a/setup/setuppage.class.inc.php b/setup/setuppage.class.inc.php index d1fbbd9b8..69b805a21 100644 --- a/setup/setuppage.class.inc.php +++ b/setup/setuppage.class.inc.php @@ -304,5 +304,3 @@ h3.clickable.open { } } } // End of class - -?> diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index a65a49fba..15d93684b 100644 --- a/setup/setuputils.class.inc.php +++ b/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 "Comparison of ".dirname($sBaseDir)."/modules:\n".print_r($aResults, true).""; +// echo "
Comparison of ".dirname($sBaseDir)."/modules against $sManifestFile:\n".print_r($aResults, true).""; 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:
".print_r($aDMchanges, true).""); + //echo("Changes detected compared to $sInstalledVersion:
".print_r($aDMchanges, true)."
".print_r($aPortalChanges, true)."
".print_r($aCodeChanges, true).""); + 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; + } } \ No newline at end of file diff --git a/setup/wizardcontroller.class.inc.php b/setup/wizardcontroller.class.inc.php index c6603b0f2..276704dee 100644 --- a/setup/wizardcontroller.class.inc.php +++ b/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("