diff --git a/setup/feature_removal/SetupAudit.php b/setup/feature_removal/SetupAudit.php index 7cdcaa3a2..35c4628d9 100644 --- a/setup/feature_removal/SetupAudit.php +++ b/setup/feature_removal/SetupAudit.php @@ -10,11 +10,11 @@ class SetupAudit extends AbstractSetupAudit private string $sEnvBefore; private string $sEnvAfter; - public function __construct(string $sEnvBefore) + public function __construct(string $sEnvBefore, ?string $sEnvAfter = null) { parent::__construct(); $this->sEnvBefore = $sEnvBefore; - $this->sEnvAfter = "$sEnvBefore-build"; + $this->sEnvAfter = $sEnvAfter ?? "$sEnvBefore-build"; } public function ComputeClasses(): void diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 80f7ad385..a8918b5cd 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -25,6 +25,7 @@ */ use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; +use Combodo\iTop\Setup\FeatureRemoval\SetupAudit; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; @@ -308,7 +309,7 @@ class RunTimeEnvironment $sModule = $oModule->GetName(); $bIsExtra = $this->GetExtensionMap()->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE); if (array_key_exists($sModule, $aAvailableModules)) { - if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect' + if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect' $aRet[$oModule->GetName()] = $oModule; } } @@ -327,9 +328,10 @@ class RunTimeEnvironment $bSelected = $oPhpExpressionEvaluator->ParseAndEvaluateBooleanExpression($oModule->GetAutoSelect()); if ($bSelected) { $aRet[$oModule->GetName()] = $oModule; // store the Id of the selected module - $bModuleAdded = true; + $bModuleAdded = true; } - } catch (ModuleFileReaderException $e) { + } + catch (ModuleFileReaderException $e) { //do nothing. logged already } } @@ -345,56 +347,6 @@ class RunTimeEnvironment return $aRet; } - /** - * Compile the data model by imitating the given environment - * The list of modules to be installed in the build environment is: - * - the list of modules present in the "source_dir" (defined by the source environment) which are marked as "installed" in the source environment's database - * - plus the list of modules present in the "extra" directory of the build environment: data/-modules/ - * - * @param string $sSourceEnv The name of the source environment to 'imitate' - * @param bool $bUseSymLinks Whether to create symbolic links instead of copies - * - * @return string[] - */ - public function CompileFrom($sSourceEnv, $bUseSymLinks = null) - { - $oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv)); - $sSourceDir = $oSourceConfig->Get('source_dir'); - - $sSourceDirFull = APPROOT.$sSourceDir; - // Do load the required modules - // - $oFactory = new ModelFactory($sSourceDirFull); - $aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir); - $oModule = null; - foreach ($aModulesToCompile as $oModule) { - if ($oModule instanceof MFDeltaModule) { - // Just before loading the delta, let's save an image of the datamodel - // in case there is no delta the operation will be done after the end of the loop - $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml'); - } - $oFactory->LoadModule($oModule); - } - - if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) { - // A delta was loaded, let's save a second copy of the datamodel - $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'-with-delta.xml'); - } else { - // No delta was loaded, let's save the datamodel now - $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml'); - } - - $sBuildDir = APPROOT.'env-'.$this->sBuildEnv; - self::MakeDirSafe($sBuildDir); - $bSkipTempDir = ($this->sFinalEnv != $this->sBuildEnv); // No need for a temporary directory if sBuildEnv is already a temporary directory - $oMFCompiler = new MFCompiler($oFactory, $this->sFinalEnv); - $oMFCompiler->Compile($sBuildDir, null, $bUseSymLinks, $bSkipTempDir); - - MetaModel::ResetAllCaches($this->sBuildEnv); - - return array_keys($aModulesToCompile); - } - /** * Helper function to create the database structure * @@ -509,7 +461,7 @@ class RunTimeEnvironment } } foreach ($aPredefinedObjects as $iRefId => $aObjValues) { - if (! array_key_exists($iRefId, $aDBIds)) { + if (!array_key_exists($iRefId, $aDBIds)) { $oNewObj = MetaModel::NewObject($sClass); $oNewObj->SetKey($iRefId); foreach ($aObjValues as $sAttCode => $value) { @@ -650,7 +602,8 @@ class RunTimeEnvironment { try { $aSelectInstall = ModuleInstallationRepository::GetInstance()->ReadFromDB($oConfig); - } catch (MySQLException $e) { + } + catch (MySQLException $e) { // No database or erroneous information $this->log_error('Can not connect to the database: host: '.$oConfig->Get('db_host').', user:'.$oConfig->Get('db_user').', pwd:'.$oConfig->Get('db_pwd').', db name:'.$oConfig->Get('db_name')); $this->log_error('Exception '.$e->getMessage()); @@ -708,14 +661,17 @@ class RunTimeEnvironment { SetupLog::Error($sText); } + protected function log_warning($sText) { SetupLog::Warning($sText); } + protected function log_info($sText) { SetupLog::Info($sText); } + protected function log_ok($sText) { SetupLog::Ok($sText); @@ -747,10 +703,16 @@ class RunTimeEnvironment if ($oLatestDM == null) { return '0.0.0'; } + return $oLatestDM->Get('version'); } - public function Commit() + /** + * Move the build env to the final env if all the steps went ok + * + * @return void + */ + public function Commit(): void { if ($this->sFinalEnv != $this->sBuildEnv) { if (file_exists(utils::GetDataPath().$this->sBuildEnv.'.delta.xml')) { @@ -808,12 +770,13 @@ class RunTimeEnvironment /** * Overwrite or create the destination file * - * @param $sSource - * @param $sDest + * @param string $sSource + * @param string $sDest * @param bool $bSourceMustExist - * @throws Exception + * + * @throws \Exception */ - protected function CommitFile($sSource, $sDest, $bSourceMustExist = true) + protected function CommitFile(string $sSource, string $sDest, bool $bSourceMustExist = true): void { if (file_exists($sSource)) { SetupUtils::builddir(dirname($sDest)); @@ -847,6 +810,7 @@ class RunTimeEnvironment * @param $sDest * @param boolean $bSourceMustExist * @param boolean $bRemoveSource If true $sSource will be removed, otherwise $sSource will just be emptied + * * @throws Exception */ protected function CommitDir($sSource, $sDest, $bSourceMustExist = true, $bRemoveSource = true) @@ -875,9 +839,11 @@ class RunTimeEnvironment /** * Call the given handler method for all selected modules having an installation handler + * * @param array[] $aAvailableModules * @param string $sHandlerName - * @param string[]|null $aSelectedModules + * @param string[]|null $aSelectedModules + * * @throws CoreException */ public function CallInstallerHandlers($aAvailableModules, $sHandlerName, $aSelectedModules = null) @@ -915,15 +881,16 @@ class RunTimeEnvironment if (is_callable($aCallSpec)) { try { call_user_func_array($aCallSpec, $aArgs); - } catch (Exception $e) { + } + catch (Exception $e) { $sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID] ?? ""; $sErrorMessage = "Module $sModuleId : error when calling module installer class $sModuleInstallerClass for $sHandlerName handler"; $aExceptionContextData = [ - 'ModulelId' => $sModuleId, - 'ModuleInstallerClass' => $sModuleInstallerClass, + 'ModulelId' => $sModuleId, + 'ModuleInstallerClass' => $sModuleInstallerClass, 'ModuleInstallerHandler' => $sHandlerName, - 'ExceptionClass' => get_class($e), - 'ExceptionMessage' => $e->getMessage(), + 'ExceptionClass' => get_class($e), + 'ExceptionMessage' => $e->getMessage(), ]; throw new CoreException($sErrorMessage, $aExceptionContextData, '', $e); } @@ -932,9 +899,10 @@ class RunTimeEnvironment /** * Load data from XML files for the selected modules (structural data and/or sample data) + * * @param array[] $aAvailableModules All available modules and their definition * @param bool $bSampleData Wether or not to load sample data - * @param null|string[] $aSelectedModules List of selected modules + * @param null|string[] $aSelectedModules List of selected modules */ public function LoadData($aAvailableModules, $bSampleData, $aSelectedModules = null) { @@ -1011,9 +979,11 @@ class RunTimeEnvironment /** * Merge two arrays of file names, adding the relative path to the files provided in the array to merge + * * @param string[] $aSourceArray * @param string $sBaseDir * @param string[] $aFilesToMerge + * * @return string[] */ protected static function MergeWithRelativeDir($aSourceArray, $sBaseDir, $aFilesToMerge) @@ -1022,14 +992,16 @@ class RunTimeEnvironment foreach ($aFilesToMerge as $sFile) { $aToMerge[] = $sBaseDir.'/'.$sFile; } + return array_merge($aSourceArray, $aToMerge); } /** * Check the MetaModel for some common pitfall (class name too long, classes requiring too many joins...) * The check takes about 900 ms for 200 classes - * @throws Exception + * * @return string + * @throws Exception */ public function CheckMetaModel() { @@ -1043,7 +1015,7 @@ class RunTimeEnvironment $oSearch = new DBObjectSearch($sClass); $oSearch->SetShowObsoleteData(false); - $oSQLQuery = $oSearch->GetSQLQueryStructure(null, false); + $oSQLQuery = $oSearch->GetSQLQueryStructure([], false); $sViewName = MetaModel::DBGetView($sClass); if (strlen($sViewName) > 64) { throw new Exception("Class name too long for class: '$sClass'. The name of the corresponding view ($sViewName) would exceed MySQL's limit for the name of a table (64 characters)."); @@ -1062,4 +1034,226 @@ class RunTimeEnvironment return sprintf("Checked %d classes in %.1f ms. No error found.\n", $iCount, $fDuration * 1000.0); } -} // End of class + + public function DataToCleanupAudit() + { + $oSetupAudit = new SetupAudit('production', $this->sBuildEnv); + + //Make sure the MetaModel is started before analysing for issues + $sConfFile = utils::GetConfigFilePath($this->sBuildEnv); + MetaModel::Startup($sConfFile, false, false, false, $this->sBuildEnv); + $oSetupAudit->GetIssues(true); + $iCount = $oSetupAudit->GetDataToCleanupCount(); + + if ($iCount > 0) { + throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", DataAuditSequencer::DATA_AUDIT_FAILED); + } + } + + public function CopySetupFiles(): void + { + $sSourceEnv = 'production'; + $sDestinationEnv = $this->sBuildEnv; + + if ($sDestinationEnv != $sSourceEnv) { + SetupUtils::CopyFile(utils::GetDataPath().$sSourceEnv.'.delta.xml', utils::GetDataPath().$sDestinationEnv.'.delta.xml'); + SetupUtils::copydir(utils::GetDataPath().$sSourceEnv.'-modules/', utils::GetDataPath().$sDestinationEnv.'-modules/'); + + // Copy the config file + // + $sFinalConfig = APPCONF.$sDestinationEnv.'/config-itop.php'; + if (is_file($sFinalConfig)) { + chmod($sFinalConfig, 0770); // In case it exists: RWX for owner and group, nothing for others + } + SetupUtils::copydir(APPCONF.$sSourceEnv, APPCONF.$sDestinationEnv); + if (is_file($sFinalConfig)) { + chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others + } + + MetaModel::ResetAllCaches($sDestinationEnv); + } + } + + /** + * Compile the data model by imitating the given environment + * The list of modules to be installed in the build environment is: + * - the list of modules present in the "source_dir" (defined by the source environment) which are marked as "installed" in the source environment's database + * - plus the list of modules present in the "extra" directory of the build environment: data/-modules/ + * + * @param string $sSourceEnv The name of the source environment to 'imitate' + * @param null $bUseSymLinks Whether to create symbolic links instead of copies + * + * @return string[] + * @throws \ConfigException + * @throws \CoreException + */ + public function CompileFrom($sSourceEnv, $bUseSymLinks = null) + { + $oSourceConfig = new Config(utils::GetConfigFilePath($sSourceEnv)); + $sSourceDir = $oSourceConfig->Get('source_dir'); + + $sSourceDirFull = APPROOT.$sSourceDir; + // Do load the required modules + // + $oFactory = new ModelFactory($sSourceDirFull); + $aModulesToCompile = $this->GetMFModulesToCompile($sSourceEnv, $sSourceDir); + $oModule = null; + foreach ($aModulesToCompile as $oModule) { + if ($oModule instanceof MFDeltaModule) { + // Just before loading the delta, let's save an image of the datamodel + // in case there is no delta the operation will be done after the end of the loop + $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml'); + } + $oFactory->LoadModule($oModule); + } + + if (!is_null($oModule) && ($oModule instanceof MFDeltaModule)) { + // A delta was loaded, let's save a second copy of the datamodel + $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'-with-delta.xml'); + } else { + // No delta was loaded, let's save the datamodel now + $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$this->sBuildEnv.'.xml'); + } + + $sBuildDir = APPROOT.'env-'.$this->sBuildEnv; + self::MakeDirSafe($sBuildDir); + $bSkipTempDir = ($this->sFinalEnv != $this->sBuildEnv); // No need for a temporary directory if sBuildEnv is already a temporary directory + $oMFCompiler = new MFCompiler($oFactory, $this->sFinalEnv); + $oMFCompiler->Compile($sBuildDir, null, $bUseSymLinks, $bSkipTempDir); + + MetaModel::ResetAllCaches($this->sBuildEnv); + + return array_keys($aModulesToCompile); + } + + + /** + * @param array $aRemovedExtensionCodes + * @param array $aSelectedModules + * @param string $sSourceDir + * @param string $sExtensionDir + * @param boolean $bUseSymbolicLinks + * + * @return void + * @throws \ConfigException + * @throws \CoreException + * + */ + public function DoCompile(array $aRemovedExtensionCodes, array $aSelectedModules, string $sSourceDir, string $sExtensionDir, bool $bUseSymbolicLinks = false): void + { + SetupLog::Info('Compiling data model.'); + + $sEnvironment = $this->sBuildEnv; + $sBuildPath = $this->GetBuildDir(); + + $sSourcePath = APPROOT.$sSourceDir; + $aDirsToScan = [$sSourcePath]; + $sExtensionsPath = APPROOT.$sExtensionDir; + if (is_dir($sExtensionsPath)) { + // if the extensions dir exists, scan it for additional modules as well + $aDirsToScan[] = $sExtensionsPath; + } + $sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/'; + if (is_dir($sExtraPath)) { + // if the extra dir exists, scan it for additional modules as well + $aDirsToScan[] = $sExtraPath; + } + + if (!is_dir($sSourcePath)) { + throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server"); + } + + if (!is_dir($sBuildPath)) { + if (!mkdir($sBuildPath)) { + throw new Exception("Failed to create directory '$sBuildPath', please check the rights of the web server"); + } else { + // adjust the rights if and only if the directory was just created + // owner:rwx user/group:rx + chmod($sBuildPath, 0755); + } + } elseif ($this->IsInItop($sBuildPath)) { + // If the directory is under the root folder - as expected - let's clean-it before compiling + SetupUtils::tidydir($sBuildPath); + } + + $oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan); + // Removed modules are stored as static for FindModules() + $oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes); + + $oFactory = new ModelFactory($aDirsToScan); + + $oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries'); + $oFactory->LoadModule($oDictModule); + + $sDeltaFile = APPROOT.'core/datamodel.core.xml'; + if (file_exists($sDeltaFile)) { + $oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile); + $oFactory->LoadModule($oCoreModule); + } + $sDeltaFile = APPROOT.'application/datamodel.application.xml'; + if (file_exists($sDeltaFile)) { + $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); + $oFactory->LoadModule($oApplicationModule); + } + + $aModules = $oFactory->FindModules(); + + foreach ($aModules as $oModule) { + $sModule = $oModule->GetName(); + if (in_array($sModule, $aSelectedModules)) { + $oFactory->LoadModule($oModule); + } + } + + // Dump the "reference" model, just before loading any actual delta + $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml'); + + $sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml'; + if (file_exists($sDeltaFile)) { + $oDelta = new MFDeltaModule($sDeltaFile); + $oFactory->LoadModule($oDelta); + $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml'); + } + + $oMFCompiler = new MFCompiler($oFactory, $sEnvironment); + $oMFCompiler->Compile($sBuildPath, null, $bUseSymbolicLinks, false, false); + SetupLog::Info("Data model successfully compiled to '$sBuildPath'."); + + $sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/'; + SetupUtils::builddir($sCacheDir); + SetupUtils::tidydir($sCacheDir); + + + // Set an "Instance UUID" identifying this machine based on a file located in the data directory + $sInstanceUUIDFile = utils::GetDataPath().'instance.txt'; + SetupUtils::builddir(utils::GetDataPath()); + if (!file_exists($sInstanceUUIDFile)) { + $sInstanceUUID = utils::CreateUUID('filesystem'); + file_put_contents($sInstanceUUIDFile, $sInstanceUUID); + } + } + + protected function IsInItop(string $sPath): bool + { + $sFileRealPath = realpath($sPath); + if ($sFileRealPath === false) { + return false; + } + + $sRealBasePath = realpath(APPROOT); // avoid problems when having '/' on Windows for example + if (!self::StartsWith($sFileRealPath, $sRealBasePath)) { + return false; + } + + return true; + } + + protected static function StartsWith(string $sHaystack, string $sNeedle): bool + { + if (strlen($sNeedle) > strlen($sHaystack)) { + return false; + } + + return substr_compare($sHaystack, $sNeedle, 0, strlen($sNeedle)) === 0; + } +} diff --git a/setup/sequencers/ApplicationInstallSequencer.php b/setup/sequencers/ApplicationInstallSequencer.php index a2a49c68b..580cc78e3 100644 --- a/setup/sequencers/ApplicationInstallSequencer.php +++ b/setup/sequencers/ApplicationInstallSequencer.php @@ -45,14 +45,22 @@ class ApplicationInstallSequencer extends StepSequencer protected Config $oConfig; + protected RunTimeEnvironment $oRunTimeEnvironment; + /** * @param \Parameters $oParams * * @throws \ConfigException * @throws \CoreException */ - public function __construct(Parameters $oParams) + public function __construct(Parameters $oParams, ?RunTimeEnvironment $oRunTimeEnvironment = null) { + if (is_null($oRunTimeEnvironment)) { + $sEnvironment = $oParams->Get('target_env', 'production'); + $oRunTimeEnvironment = new RunTimeEnvironment($sEnvironment, false); + } + $this->oRunTimeEnvironment = $oRunTimeEnvironment; + $this->oParams = $oParams; $aParamValues = $oParams->GetParamForConfigArray(); @@ -99,26 +107,7 @@ class ApplicationInstallSequencer extends StepSequencer return $oConfig; } - protected function DoLogParameters($sPrefix = 'install-', $sOperation = 'Installation') - { - // Log the parameters... - $oDoc = new DOMDocument('1.0', 'UTF-8'); - $oDoc->preserveWhiteSpace = false; - $oDoc->formatOutput = true; - $this->oParams->ToXML($oDoc, null, 'installation'); - $sXML = $oDoc->saveXML(); - $sSafeXml = preg_replace("|([^<]*)|", "**removed**", $sXML); - SetupLog::Info("======= ".$sOperation." starts =======\nParameters:\n$sSafeXml\n"); - // Save the response file as a stand-alone file as well - $sFileName = $sPrefix.date('Y-m-d'); - $index = 0; - while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) { - $index++; - $sFileName = $sPrefix.date('Y-m-d').'-'.$index; - } - file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml); - } /** * Executes the next step of the installation and reports about the progress @@ -137,6 +126,14 @@ class ApplicationInstallSequencer extends StepSequencer $this->EnterReadOnlyMode(); switch ($sStep) { case '': + if (in_array('log-parameters', $this->oParams->Get('optional_steps', []))) { + return $this->GetNextStep('log-parameters', 'Log parameters', 0); + } + return $this->GetNextStep('copy', 'Copying data model files', 5); + + case 'log-parameters': + $this->DoLogParameters('data-audit-', 'Data Audit'); + return $this->GetNextStep('copy', 'Copying data model files', 5); $this->DoLogParameters(); @@ -374,26 +371,6 @@ class ApplicationInstallSequencer extends StepSequencer SetupUtils::ExitReadOnlyMode(); } - protected function DoCopy($aCopies) - { - $aReports = []; - foreach ($aCopies as $aCopy) { - $sSource = $aCopy['source']; - $sDestination = APPROOT.$aCopy['destination']; - - SetupUtils::builddir($sDestination); - SetupUtils::tidydir($sDestination); - SetupUtils::copydir($sSource, $sDestination); - $aReports[] = "'{$aCopy['source']}' to '{$aCopy['destination']}' (OK)"; - } - if (count($aReports) > 0) { - $sReport = "Copies: ".count($aReports).': '.implode('; ', $aReports); - } else { - $sReport = "No file copy"; - } - return $sReport; - } - /** * @param string $sBackupFileFormat * @param string $sSourceConfigFile @@ -416,160 +393,6 @@ class ApplicationInstallSequencer extends StepSequencer $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); } - /** - * @param array $aRemovedExtensionCodes - * @param array $aSelectedModules - * @param string $sSourceDir - * @param string $sExtensionDir - * @param boolean $bUseSymbolicLinks - * - * @return void - * @throws \ConfigException - * @throws \CoreException - * - * @since 3.1.0 N°2013 added the aParamValues param - */ - protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null, $bEnterMaintenanceMode = true) - { - /** - * @since 3.2.0 move the ContextTag init at the very beginning of the method - * @noinspection PhpUnusedLocalVariableInspection - */ - $oContextTag = new ContextTag(ContextTag::TAG_SETUP); - - SetupLog::Info("Compiling data model."); - - require_once(APPROOT.'setup/modulediscovery.class.inc.php'); - require_once(APPROOT.'setup/modelfactory.class.inc.php'); - require_once(APPROOT.'setup/compiler.class.inc.php'); - - $aParamValues = $this->oParams->GetParamForConfigArray(); - $sEnvironment = $this->GetTargetEnv(); - $sTargetDir = $this->GetTargetDir(); - - if (empty($sSourceDir) || empty($sTargetDir)) { - throw new Exception("missing parameter source_dir and/or target_dir"); - } - - $sSourcePath = APPROOT.$sSourceDir; - $aDirsToScan = [$sSourcePath]; - $sExtensionsPath = APPROOT.$sExtensionDir; - if (is_dir($sExtensionsPath)) { - // if the extensions dir exists, scan it for additional modules as well - $aDirsToScan[] = $sExtensionsPath; - } - $sExtraPath = APPROOT.'/data/'.$sEnvironment.'-modules/'; - if (is_dir($sExtraPath)) { - // if the extra dir exists, scan it for additional modules as well - $aDirsToScan[] = $sExtraPath; - } - $sTargetPath = APPROOT.$sTargetDir; - - if (!is_dir($sSourcePath)) { - throw new Exception("Failed to find the source directory '$sSourcePath', please check the rights of the web server"); - } - - $bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode(); - - if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) { - $sConfigFilePath = utils::GetConfigFilePath($sEnvironment); - if (is_file($sConfigFilePath)) { - $oConfig = new Config($sConfigFilePath); - $oConfig->UpdateFromParams($aParamValues); - if ($bEnterMaintenanceMode) { - SetupUtils::EnterMaintenanceMode($oConfig); - } - } - } - try { - if (!is_dir($sTargetPath)) { - if (!mkdir($sTargetPath)) { - throw new Exception("Failed to create directory '$sTargetPath', please check the rights of the web server"); - } else { - // adjust the rights if and only if the directory was just created - // owner:rwx user/group:rx - chmod($sTargetPath, 0755); - } - } elseif (substr($sTargetPath, 0, strlen(APPROOT)) == APPROOT) { - // If the directory is under the root folder - as expected - let's clean-it before compiling - SetupUtils::tidydir($sTargetPath); - } - - $oExtensionsMap = new iTopExtensionsMap('production', $aDirsToScan); - $oExtensionsMap->DeclareExtensionAsRemoved($aRemovedExtensionCodes); - - $oFactory = new ModelFactory($aDirsToScan); - - $oDictModule = new MFDictModule('dictionaries', 'iTop Dictionaries', APPROOT.'dictionaries'); - $oFactory->LoadModule($oDictModule); - - $sDeltaFile = APPROOT.'core/datamodel.core.xml'; - if (file_exists($sDeltaFile)) { - $oCoreModule = new MFCoreModule('core', 'Core Module', $sDeltaFile); - $oFactory->LoadModule($oCoreModule); - } - $sDeltaFile = APPROOT.'application/datamodel.application.xml'; - if (file_exists($sDeltaFile)) { - $oApplicationModule = new MFCoreModule('application', 'Application Module', $sDeltaFile); - $oFactory->LoadModule($oApplicationModule); - } - - $aModules = $oFactory->FindModules(); - - foreach ($aModules as $oModule) { - $sModule = $oModule->GetName(); - if (in_array($sModule, $aSelectedModules)) { - $oFactory->LoadModule($oModule); - } - } - // Dump the "reference" model, just before loading any actual delta - $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'.xml'); - - $sDeltaFile = utils::GetDataPath().$sEnvironment.'.delta.xml'; - if (file_exists($sDeltaFile)) { - $oDelta = new MFDeltaModule($sDeltaFile); - $oFactory->LoadModule($oDelta); - $oFactory->SaveToFile(utils::GetDataPath().'datamodel-'.$sEnvironment.'-with-delta.xml'); - } - - $oMFCompiler = new MFCompiler($oFactory, $sEnvironment); - $oMFCompiler->Compile($sTargetPath, null, $bUseSymbolicLinks, false, $bEnterMaintenanceMode); - //$aCompilerLog = $oMFCompiler->GetLog(); - //SetupLog::Info(implode("\n", $aCompilerLog)); - SetupLog::Info("Data model successfully compiled to '$sTargetPath'."); - - $sCacheDir = APPROOT.'/data/cache-'.$sEnvironment.'/'; - SetupUtils::builddir($sCacheDir); - SetupUtils::tidydir($sCacheDir); - } catch (Exception $e) { - if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) { - SetupUtils::ExitMaintenanceMode(); - } - throw $e; - } - - // 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); - } - - // Set an "Instance UUID" identifying this machine based on a file located in the data directory - $sInstanceUUIDFile = utils::GetDataPath().'instance.txt'; - SetupUtils::builddir(utils::GetDataPath()); - if (!file_exists($sInstanceUUIDFile)) { - $sIntanceUUID = utils::CreateUUID('filesystem'); - file_put_contents($sInstanceUUIDFile, $sIntanceUUID); - } - if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode && $bEnterMaintenanceMode) { - SetupUtils::ExitMaintenanceMode(); - } - } - protected function GetModelInfoPath(string $sEnv): string { return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json"; diff --git a/setup/sequencers/DataAuditSequencer.php b/setup/sequencers/DataAuditSequencer.php index bea3bf0f8..1bc6fcfd0 100644 --- a/setup/sequencers/DataAuditSequencer.php +++ b/setup/sequencers/DataAuditSequencer.php @@ -25,9 +25,6 @@ require_once APPROOT.'setup/feature_removal/SetupAudit.php'; require_once(APPROOT.'setup/sequencers/StepSequencer.php'); require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php'); -use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment; -use Combodo\iTop\Setup\FeatureRemoval\SetupAudit; - class DataAuditSequencer extends ApplicationInstallSequencer { public const DATA_AUDIT_FAILED = 100; @@ -35,12 +32,14 @@ class DataAuditSequencer extends ApplicationInstallSequencer protected function GetTempEnv() { $sTargetEnv = $this->GetTargetEnv(); + return $sTargetEnv.'-build'; } protected function GetTargetDir() { $sTargetEnv = $this->GetTempEnv(); + return 'env-'.$sTargetEnv; } @@ -56,102 +55,56 @@ class DataAuditSequencer extends ApplicationInstallSequencer public function ExecuteStep($sStep = '', $sInstallComment = null) { try { + /** + * @since 3.2.0 move the ContextTag init at the very beginning of the method + * @noinspection PhpUnusedLocalVariableInspection + */ + $oContextTag = new ContextTag(ContextTag::TAG_SETUP); $fStart = microtime(true); SetupLog::Info("##### STEP {$sStep} start"); switch ($sStep) { case '': - $this->DoLogParameters('data-audit-', 'Data Audit'); + return $this->GetNextStep('copy', 'Copying data model files', 5); - $aResult = [ - 'status' => self::OK, - 'message' => '', - 'percentage-completed' => 20, - 'next-step' => 'compile', - 'next-step-label' => 'Compiling the data model', - ]; - - break; + case 'copy': + $this->oRunTimeEnvironment->CopySetupFiles(); + return $this->GetNextStep('compile', 'Compiling the data model', 20, 'Copying...'); case 'compile': $aSelectedModules = $this->oParams->Get('selected_modules'); $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest'); $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions'); + $aMiscOptions = $this->oParams->Get('options', []); $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []); - - $this->DoCompile( + $bUseSymbolicLinks = $aMiscOptions['symlinks'] ?? false; + $sMessage = $bUseSymbolicLinks ? '' : 'Using symbolic links instead of copying data model files (for developers only!)'; + $this->oRunTimeEnvironment->DoCompile( $aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, - false, - false + $bUseSymbolicLinks ); + return $this->GetNextStep('setup-audit', 'Checking data consistency with the new data model', 70, $sMessage); - $aResult = [ - 'status' => self::OK, - 'message' => '', - 'next-step' => 'write-config', - 'next-step-label' => 'Writing audit config', - 'percentage-completed' => 40, - ]; - break; - case 'write-config': - $this->DoWriteConfig(); - $aResult = [ - 'status' => self::OK, - 'message' => '', - 'next-step' => 'setup-audit', - 'next-step-label' => 'Checking data consistency with the new data model', - 'percentage-completed' => 60, - ]; - break; case 'setup-audit': - $this->DoSetupAudit(); - $aResult = [ - 'status' => self::OK, - 'message' => '', - 'next-step' => 'cleanup', - 'next-step-label' => 'Temporary folders cleanup', - 'percentage-completed' => 80, - ]; - break; - case 'cleanup' : - $this->DoCleanup(); - $aResult = [ - 'status' => self::OK, - 'message' => '', - 'next-step' => '', - 'next-step-label' => 'Completed', - 'percentage-completed' => 100, - ]; - break; - default: - $aResult = [ - 'status' => self::ERROR, - 'message' => '', - 'next-step' => '', - 'next-step-label' => "Unknown setup step '$sStep'.", - 'percentage-completed' => 100, - ]; - break; - } - } catch (Exception $e) { - $aResult = [ - 'status' => self::ERROR, - 'message' => $e->getMessage(), - 'next-step' => '', - 'next-step-label' => '', - 'percentage-completed' => 100, - 'error_code' => $e->getCode(), - ]; + $this->oRunTimeEnvironment->DataToCleanupAudit(); + return $this->GetNextStep('', 'Completed', 100); + default: + return $this->GetNextStep('', "Unknown setup step '$sStep'.", 100, '', self::ERROR); + } + } + catch (Exception $e) { $this->ReportException($e); - } finally { + $aResult = $this->GetNextStep('', '', 100, $e->getMessage(), self::ERROR); + $aResult['error_code'] = $e->getCode(); + return $aResult; + } + finally { $fDuration = round(microtime(true) - $fStart, 2); SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s"); } - - return $aResult; } protected function DoWriteConfig() @@ -167,57 +120,7 @@ class DataAuditSequencer extends ApplicationInstallSequencer return $oConfig->WriteToFile($sTempConfigFileName); } + return false; } - - protected function DoSetupAudit() - { - /** - * @since 3.2.0 move the ContextTag init at the very beginning of the method - * @noinspection PhpUnusedLocalVariableInspection - */ - $oContextTag = new ContextTag(ContextTag::TAG_SETUP); - - $sPreviousEnvironment = $this->GetTargetEnv(); - - $oSetupAudit = new SetupAudit($sPreviousEnvironment); - - //Make sure the MetaModel is started before analysing for issues - $sConfFile = utils::GetConfigFilePath($sPreviousEnvironment); - MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sPreviousEnvironment); - $oSetupAudit->GetIssues(true); - $iCount = $oSetupAudit->GetDataToCleanupCount(); - - if ($iCount > 0) { - throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice prior to upgrading iTop", static::DATA_AUDIT_FAILED); - } - } - - protected function DoCleanup() - { - $sEnv = $this->GetTempEnv(); - - //keep this folder empty - SetupUtils::tidydir(APPROOT."/env-$sEnv"); - - $aFolders = [ - APPROOT."/data/$sEnv-modules", - APPROOT."/data/cache-$sEnv", - APPROOT."/conf/$sEnv", - ]; - foreach ($aFolders as $sFolder) { - SetupUtils::tidydir($sFolder); - SetupUtils::rmdir_safe($sFolder); - } - - $sFiles = [ - APPROOT."/data/datamodel-$sEnv.xml", - APPROOT."/data/$sEnv.delta.prev.xml", - ]; - foreach ($sFiles as $sFile) { - if (is_file($sFile)) { - @unlink($sFile); - } - } - } } diff --git a/setup/sequencers/StepSequencer.php b/setup/sequencers/StepSequencer.php index ca9f83d08..53f72666f 100644 --- a/setup/sequencers/StepSequencer.php +++ b/setup/sequencers/StepSequencer.php @@ -102,5 +102,37 @@ abstract class StepSequencer return ($iOverallStatus == self::OK); } + protected function GetNextStep(string $sNextStep, string $sNextStepLabel, int $iPercentComplete, string $sMessage = '', int $iStatus = self::OK): array + { + return [ + 'status' => $iStatus, + 'message' => $sMessage, + 'next-step' => $sNextStep, + 'next-step-label' => $sNextStepLabel, + 'percentage-completed' => $iPercentComplete, + ]; + } + + protected function DoLogParameters($sPrefix = 'install-', $sOperation = 'Installation') + { + // Log the parameters... + $oDoc = new DOMDocument('1.0', 'UTF-8'); + $oDoc->preserveWhiteSpace = false; + $oDoc->formatOutput = true; + $this->oParams->ToXML($oDoc, null, 'installation'); + $sXML = $oDoc->saveXML(); + $sSafeXml = preg_replace('|([^<]*)|', '**removed**', $sXML); + SetupLog::Info('======= '.$sOperation." starts =======\nParameters:\n$sSafeXml\n"); + + // Save the response file as a stand-alone file as well + $sFileName = $sPrefix.date('Y-m-d'); + $index = 0; + while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) { + $index++; + $sFileName = $sPrefix.date('Y-m-d').'-'.$index; + } + file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml); + } + abstract public function ExecuteStep($sStep = '', $sComment = null); } diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index 60b2fa43f..f10f8532a 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -817,6 +817,11 @@ class SetupUtils } } + public static function CopyFile(string $sSource, string $sDest, bool $bUseSymbolicLinks = false): bool + { + return self::copydir($sSource, $sDest, $bUseSymbolicLinks); + } + /** * Helper to copy a directory to a target directory, skipping .SVN files (for developer's comfort!) * Returns true if successful @@ -826,11 +831,13 @@ class SetupUtils * @return bool * @throws Exception */ - public static function copydir($sSource, $sDest, $bUseSymbolicLinks = false) + public static function copydir(string $sSource, string $sDest, bool $bUseSymbolicLinks = false): bool { if (is_dir($sSource)) { if (!is_dir($sDest)) { mkdir($sDest, 0777 /* Default */, true); + } else { + SetupUtils::tidydir($sDest); } $aFiles = scandir($sSource); if (sizeof($aFiles) > 0) { @@ -839,24 +846,13 @@ class SetupUtils // Skip continue; } - if (is_dir($sSource.'/'.$sFile)) { // Recurse self::copydir($sSource.'/'.$sFile, $sDest.'/'.$sFile, $bUseSymbolicLinks); } else { if ($bUseSymbolicLinks) { - if (function_exists('symlink')) { - if (file_exists($sDest.'/'.$sFile)) { - unlink($sDest.'/'.$sFile); - } - symlink($sSource.'/'.$sFile, $sDest.'/'.$sFile); - } else { - throw(new Exception("Error, cannot *copy* '$sSource/$sFile' to '$sDest/$sFile' using symbolic links, 'symlink' is not supported on this system.")); - } + symlink($sSource.'/'.$sFile, $sDest.'/'.$sFile); } else { - if (is_link($sDest.'/'.$sFile)) { - unlink($sDest.'/'.$sFile); - } copy($sSource.'/'.$sFile, $sDest.'/'.$sFile); } } @@ -865,11 +861,7 @@ class SetupUtils return true; } elseif (is_file($sSource)) { if ($bUseSymbolicLinks) { - if (function_exists('symlink')) { - return symlink($sSource, $sDest); - } else { - throw(new Exception("Error, cannot *copy* '$sSource' to '$sDest' using symbolic links, 'symlink' is not supported on this system.")); - } + return symlink($sSource, $sDest); } else { return copy($sSource, $sDest); } diff --git a/setup/wizardsteps/AbstractWizStepInstall.php b/setup/wizardsteps/AbstractWizStepInstall.php index e422a4359..febc309e2 100644 --- a/setup/wizardsteps/AbstractWizStepInstall.php +++ b/setup/wizardsteps/AbstractWizStepInstall.php @@ -67,6 +67,10 @@ abstract class AbstractWizStepInstall extends WizardStep 'copies' => $aCopies, // 'backup' => see below ], + 'optional_steps' => [ + '' + // 'backup' => see below + ], 'source_dir' => str_replace(APPROOT, '', $sSourceDir), 'datamodel_version' => $this->oWizard->GetParameter('datamodel_version'), //TODO: let the installer compute this automatically... 'previous_configuration_file' => $sPreviousConfigurationFile,