diff --git a/setup/SetupDBBackup.php b/setup/SetupDBBackup.php
new file mode 100644
index 000000000..c6f95fa91
--- /dev/null
+++ b/setup/SetupDBBackup.php
@@ -0,0 +1,14 @@
+
-
-use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
-use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
-use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
-use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
-
-require_once(APPROOT.'setup/parameters.class.inc.php');
-require_once(APPROOT.'setup/StepSequencer.php');
-require_once(APPROOT.'setup/xmldataloader.class.inc.php');
-require_once(APPROOT.'setup/backup.class.inc.php');
-require_once APPROOT.'setup/feature_removal/SetupAudit.php';
-require_once APPROOT.'setup/feature_removal/InplaceSetupAudit.php';
-require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
-
/**
- * The base class for the installation process.
- * The installation process is split into a sequence of unitary steps
- * for performance reasons (i.e; timeout, memory usage) and also in order
- * to provide some feedback about the progress of the installation.
+ * Copyright (C) 2013-2026 Combodo SAS
*
- * This class can be used for a step by step interactive installation
- * while displaying a progress bar, or in an unattended manner
- * (for example from the command line), to run all the steps
- * in one go.
- * @copyright Copyright (C) 2010-2024 Combodo SAS
- * @license http://opensource.org/licenses/AGPL-3.0
+ * This file is part of iTop.
+ *
+ * iTop is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * iTop is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
*/
-class ApplicationInstallSequencer extends StepSequencer
-{
- /** @var \Parameters */
- protected $oParams;
- protected static $bMetaModelStarted = false;
-
- protected Config $oConfig;
-
- /**
- * @param \Parameters $oParams
- *
- * @throws \ConfigException
- * @throws \CoreException
- */
- public function __construct($oParams)
- {
- $this->oParams = $oParams;
-
- $aParamValues = $oParams->GetParamForConfigArray();
- $this->oConfig = new Config();
- $this->oConfig->UpdateFromParams($aParamValues);
- utils::SetConfig($this->oConfig);
- }
-
- /**
- * @return string
- */
- protected function GetTargetEnv()
- {
- $sTargetEnvironment = $this->oParams->Get('target_env', '');
- if ($sTargetEnvironment !== '') {
- return $sTargetEnvironment;
- }
-
- return 'production';
- }
-
- /**
- * @return string
- */
- protected function GetTargetDir()
- {
- $sTargetEnv = $this->GetTargetEnv();
- return 'env-'.$sTargetEnv;
- }
-
- protected function GetConfig()
- {
- $sTargetEnvironment = $this->GetTargetEnv();
- $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE;
- try {
- $oConfig = new Config($sConfigFile);
- } catch (Exception $e) {
- return null;
- }
-
- $aParamValues = $this->oParams->GetParamForConfigArray();
- $oConfig->UpdateFromParams($aParamValues);
-
- return $oConfig;
- }
-
- /**
- * Executes the next step of the installation and reports about the progress
- * and the next step to perform
- *
- * @param string $sStep The identifier of the step to execute
- * @param string|null $sInstallComment
- *
- * @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
- */
- public function ExecuteStep($sStep = '', $sInstallComment = null)
- {
- try {
- $fStart = microtime(true);
- SetupLog::Info("##### STEP {$sStep} start");
- $this->EnterReadOnlyMode();
- switch ($sStep) {
- case '':
- $aResult = [
- 'status' => self::OK,
- 'message' => '',
- 'percentage-completed' => 0,
- 'next-step' => 'copy',
- 'next-step-label' => 'Copying data model files',
- ];
-
- // 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("======= Installation starts =======\nParameters:\n$sSafeXml\n");
-
- // Save the response file as a stand-alone file as well
- $sFileName = 'install-'.date('Y-m-d');
- $index = 0;
- while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
- $index++;
- $sFileName = 'install-'.date('Y-m-d').'-'.$index;
- }
- file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
-
- break;
-
- case 'copy':
- $aPreinstall = $this->oParams->Get('preinstall');
- $aCopies = $aPreinstall['copies'] ?? [];
-
- $this->DoCopy($aCopies);
- $sReport = "Copying...";
-
- $aResult = [
- 'status' => self::OK,
- 'message' => $sReport,
- ];
- if (isset($aPreinstall['backup'])) {
- $aResult['next-step'] = 'backup';
- $aResult['next-step-label'] = 'Performing a backup of the database';
- $aResult['percentage-completed'] = 20;
- } else {
- $aResult['next-step'] = 'compile';
- $aResult['next-step-label'] = 'Compiling the data model';
- $aResult['percentage-completed'] = 20;
- }
- break;
-
- case 'backup':
- $aPreinstall = $this->oParams->Get('preinstall');
- // __DB__-%Y-%m-%d
- $sDestination = $aPreinstall['backup']['destination'];
- $sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
- $sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
- $this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
-
- $aResult = [
- 'status' => self::OK,
- 'message' => "Created backup",
- 'next-step' => 'compile',
- 'next-step-label' => 'Compiling the data model',
- 'percentage-completed' => 20,
- ];
- break;
-
- 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', []);
-
- $bUseSymbolicLinks = null;
- if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
- if (function_exists('symlink')) {
- $bUseSymbolicLinks = true;
- SetupLog::Info("Using symbolic links instead of copying data model files (for developers only!)");
- } else {
- SetupLog::Info("Symbolic links (function symlinks) does not seem to be supported on this platform (OS/PHP version).");
- }
- }
-
- $this->DoCompile(
- $aRemovedExtensionCodes,
- $aSelectedModules,
- $sSourceDir,
- $sExtensionDir,
- $bUseSymbolicLinks
- );
-
- $sNextStep = 'db-schema';
- $sNextStepLabel = 'Updating database schema';
-
- $aResult = [
- 'status' => self::OK,
- 'message' => '',
- 'next-step' => $sNextStep,
- 'next-step-label' => $sNextStepLabel,
- 'percentage-completed' => 40,
- ];
- break;
-
- case 'db-schema':
- $aSelectedModules = $this->oParams->Get('selected_modules', []);
-
- $this->DoUpdateDBSchema(
- $aSelectedModules
- );
-
- $aResult = [
- 'status' => self::OK,
- 'message' => '',
- 'next-step' => 'after-db-create',
- 'next-step-label' => 'Creating profiles',
- 'percentage-completed' => 60,
- ];
- break;
-
- case 'after-db-create':
- $aAdminParams = $this->oParams->Get('admin_account');
- $sAdminUser = $aAdminParams['user'];
- $sAdminPwd = $aAdminParams['pwd'];
- $sAdminLanguage = $aAdminParams['language'];
- $aSelectedModules = $this->oParams->Get('selected_modules', []);
-
- $this->AfterDBCreate(
- $sAdminUser,
- $sAdminPwd,
- $sAdminLanguage,
- $aSelectedModules
- );
-
- $aResult = [
- 'status' => self::OK,
- 'message' => '',
- 'next-step' => 'load-data',
- 'next-step-label' => 'Loading data',
- 'percentage-completed' => 80,
- ];
- break;
-
- case 'load-data':
- $aSelectedModules = $this->oParams->Get('selected_modules');
- $bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
-
- $this->DoLoadFiles(
- $aSelectedModules,
- $bSampleData
- );
-
- $aResult = [
- 'status' => self::INFO,
- 'message' => 'All data loaded',
- 'next-step' => 'create-config',
- 'next-step-label' => 'Creating the configuration File',
- 'percentage-completed' => 99,
- ];
- break;
-
- case 'create-config':
- $sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
- $sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
- $aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
- $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
-
- $this->DoCreateConfig(
- $sPreviousConfigFile,
- $sDataModelVersion,
- $aSelectedModuleCodes,
- $aSelectedExtensionCodes,
- $sInstallComment
- );
-
- $aResult = [
- 'status' => self::INFO,
- 'message' => 'Configuration file created',
- 'next-step' => '',
- 'next-step-label' => 'Completed',
- 'percentage-completed' => 100,
- ];
- $this->ExitReadOnlyMode();
- 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,
- ];
-
- SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
- $idx = 0;
- // Log the call stack, but not the parameters since they may contain passwords or other sensitive data
- SetupLog::Ok("Call stack:");
- foreach ($e->getTrace() as $aTrace) {
- $sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
- $sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
- $sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
- $sType = empty($aTrace['type']) ? "" : $aTrace['type'];
- $sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
- $sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
- SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
- $idx++;
- }
- } finally {
- $fDuration = round(microtime(true) - $fStart, 2);
- SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
- }
-
- return $aResult;
- }
-
- protected function EnterReadOnlyMode()
- {
- if ($this->GetTargetEnv() != 'production') {
- return;
- }
-
- if (SetupUtils::IsInReadOnlyMode()) {
- return;
- }
-
- SetupUtils::EnterReadOnlyMode($this->GetConfig());
- }
-
- protected function ExitReadOnlyMode()
- {
- if ($this->GetTargetEnv() != 'production') {
- return;
- }
-
- if (!SetupUtils::IsInReadOnlyMode()) {
- return;
- }
-
- 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
- * @param string $sMySQLBinDir
- *
- * @throws \BackupException
- * @throws \CoreException
- * @throws \MySQLException
- * @since 2.5.0 uses a {@link Config} object to store DB parameters
- */
- protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
- {
- $oBackup = new SetupDBBackup($this->oConfig);
- $sTargetFile = $oBackup->MakeName($sBackupFileFormat);
- if (!empty($sMySQLBinDir)) {
- $oBackup->SetMySQLBinDir($sMySQLBinDir);
- }
-
- CMDBSource::InitFromConfig($this->oConfig);
- $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)
- {
- /**
- * @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);
- 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);
- //$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) {
- SetupUtils::ExitMaintenanceMode();
- }
- }
-
- protected function GetModelInfoPath(string $sEnv): string
- {
- return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
- }
-
- protected function SaveModelInfo(string $sEnvironment): bool
- {
- $sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
- try {
- $aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
- } catch (Exception $e) {
- //logged already
- return is_file($sModelInfoPath);
- }
-
- return (bool) file_put_contents($sModelInfoPath, json_encode($aModelInfo));
- }
-
- protected function GetPreviousModelInfo(string $sEnvironment): array
- {
- $sContent = file_get_contents($this->GetModelInfoPath($sEnvironment));
- $aModelInfo = json_decode($sContent, true);
-
- if (false === $aModelInfo) {
- throw new Exception("Could not read (before compilation) previous model to audit data");
- }
-
- return $aModelInfo;
- }
-
- protected function IsSetupDataAuditEnabled($sSkipDataAudit, array $aParamValues): bool
- {
- if ($sSkipDataAudit === "checked") {
- SetupLog::Info("Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
- return false;
- }
-
- $sMode = $aParamValues['mode'];
- if ($sMode !== "upgrade") {
- //first install
- return false;
- }
-
- $sPath = APPROOT.$this->GetTargetDir();
- if (!is_dir($sPath)) {
- SetupLog::Info("Reinstallation of an iTop from a backup (No ".$this->GetTargetDir()." found). Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
-
- return false;
- }
-
- return true;
- }
-
- /**
- * @param $aSelectedModules
- *
- * @throws \ConfigException
- * @throws \CoreException
- * @throws \MySQLException
- */
- protected function DoUpdateDBSchema($aSelectedModules)
- {
- $sTargetEnvironment = $this->GetTargetEnv();
- $sModulesDir = $this->GetTargetDir();
- $aParamValues = $this->oParams->GetParamForConfigArray();
- /**
- * @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("Update Database Schema for environment '$sTargetEnvironment'.");
- $sMode = $aParamValues['mode'];
- $sDBPrefix = $aParamValues['db_prefix'];
- $sDBName = $aParamValues['db_name'];
-
- $oConfig = new Config();
- $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
- $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
- $oProductionEnv->InitDataModel($oConfig, true); // load data model only
-
- // Migrate columns
- self::MoveColumns($sDBPrefix);
-
- // Migrate application data format
- //
- // priv_internalUser caused troubles because MySQL transforms table names to lower case under Windows
- // This becomes an issue when moving your installation data to/from Windows
- // Starting 2.0, all table names must be lowercase
- if ($sMode != 'install') {
- SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)");
- // This command will have no effect under Windows...
- // and it has been written in two steps so as to make it work under windows!
- CMDBSource::SelectDB($sDBName);
- try {
- $sRepair = "RENAME TABLE `{$sDBPrefix}priv_internalUser` TO `{$sDBPrefix}priv_internaluser_other`, `{$sDBPrefix}priv_internaluser_other` TO `{$sDBPrefix}priv_internaluser`";
- CMDBSource::Query($sRepair);
- } catch (Exception $e) {
- SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)");
- }
-
- // let's remove the records in priv_change which have no counterpart in priv_changeop
- SetupLog::Info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records");
- CMDBSource::SelectDB($sDBName);
- try {
- $sTotalCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change`";
- $iTotalCount = (int)CMDBSource::QueryToScalar($sTotalCount);
- SetupLog::Info("There is a total of $iTotalCount records in {$sDBPrefix}priv_change.");
-
- $sOrphanCount = "SELECT COUNT(c.id) FROM `{$sDBPrefix}priv_change` AS c left join `{$sDBPrefix}priv_changeop` AS o ON c.id = o.changeid WHERE o.id IS NULL";
- $iOrphanCount = (int)CMDBSource::QueryToScalar($sOrphanCount);
- SetupLog::Info("There are $iOrphanCount useless records in {$sDBPrefix}priv_change (".sprintf('%.2f', ((100.0 * $iOrphanCount) / $iTotalCount))."%)");
- if ($iOrphanCount > 0) {
- //N°3793
- if ($iOrphanCount > 100000) {
- SetupLog::Info("There are too much useless records ($iOrphanCount) in {$sDBPrefix}priv_change. Cleanup cannot be done during setup.");
- } else {
- SetupLog::Info("Removing the orphan records...");
- $sCleanup = "DELETE FROM `{$sDBPrefix}priv_change` USING `{$sDBPrefix}priv_change` LEFT JOIN `{$sDBPrefix}priv_changeop` ON `{$sDBPrefix}priv_change`.id = `{$sDBPrefix}priv_changeop`.changeid WHERE `{$sDBPrefix}priv_changeop`.id IS NULL;";
- CMDBSource::Query($sCleanup);
- SetupLog::Info("Cleanup completed successfully.");
- }
- } else {
- SetupLog::Info("Ok, nothing to cleanup.");
- }
- } catch (Exception $e) {
- SetupLog::Info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage());
- }
-
- }
-
- // Module specific actions (migrate the data)
- $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
- $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
-
- if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
- throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
- }
-
- // Set a DBProperty with a unique ID to identify this instance of iTop
- $sUUID = DBProperty::GetProperty('database_uuid', '');
- if ($sUUID === '') {
- $sUUID = utils::CreateUUID('database');
- DBProperty::SetProperty('database_uuid', $sUUID, 'Installation/upgrade of '.ITOP_APPLICATION, 'Unique ID of this '.ITOP_APPLICATION.' Database');
- }
-
- // priv_change now has an 'origin' field to distinguish between the various input sources
- // Let's initialize the field with 'interactive' for all records were it's null
- // Then check if some records should hold a different value, based on a pattern matching in the userinfo field
- CMDBSource::SelectDB($sDBName);
- try {
- $sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change` WHERE `origin` IS NULL";
- $iCount = (int)CMDBSource::QueryToScalar($sCount);
- if ($iCount > 0) {
- SetupLog::Info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)");
-
- // By default all uninitialized values are considered as 'interactive'
- $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'interactive' WHERE `origin` IS NULL";
- CMDBSource::Query($sInit);
-
- // CSV Import was identified by the comment at the end
- $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-import.php' WHERE `userinfo` LIKE '%Web Service (CSV)'";
- CMDBSource::Query($sInit);
-
- // CSV Import was identified by the comment at the end
- $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-interactive' WHERE `userinfo` LIKE '%(CSV)' AND origin = 'interactive'";
- CMDBSource::Query($sInit);
-
- // Syncho data sources were identified by the comment at the end
- // Unfortunately the comment is localized, so we have to search for all possible patterns
- $sCurrentLanguage = Dict::GetUserLanguage();
- $aSuffixes = [];
- foreach (array_keys(Dict::GetLanguages()) as $sLangCode) {
- Dict::SetUserLanguage($sLangCode);
- $sSuffix = CMDBSource::Quote('%'.Dict::S('Core:SyncDataExchangeComment'));
- $aSuffixes[$sSuffix] = true;
- }
- Dict::SetUserLanguage($sCurrentLanguage);
- $sCondition = "`userinfo` LIKE ".implode(" OR `userinfo` LIKE ", array_keys($aSuffixes));
-
- $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)";
- CMDBSource::Query($sInit);
-
- SetupLog::Info("Initialization of '{$sDBPrefix}priv_change.origin' completed.");
- } else {
- SetupLog::Info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do.");
- }
- } catch (Exception $e) {
- SetupLog::Error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage());
- }
-
- // priv_async_task now has a 'status' field to distinguish between the various statuses rather than just relying on the date columns
- // Let's initialize the field with 'planned' or 'error' for all records were it's null
- CMDBSource::SelectDB($sDBName);
- try {
- $sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_async_task` WHERE `status` IS NULL";
- $iCount = (int)CMDBSource::QueryToScalar($sCount);
- if ($iCount > 0) {
- SetupLog::Info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)");
-
- $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'planned' WHERE (`status` IS NULL) AND (`started` IS NULL)";
- CMDBSource::Query($sInit);
-
- $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'error' WHERE (`status` IS NULL) AND (`started` IS NOT NULL)";
- CMDBSource::Query($sInit);
-
- SetupLog::Info("Initialization of '{$sDBPrefix}priv_async_task.status' completed.");
- } else {
- SetupLog::Info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do.");
- }
- } catch (Exception $e) {
- SetupLog::Error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage());
- }
-
- SetupLog::Info("Database Schema Successfully Updated for environment '$sTargetEnvironment'.");
- }
-
- /**
- * @param string $sDBPrefix
- *
- * @throws \CoreException
- * @throws \MySQLException
- */
- protected static function MoveColumns($sDBPrefix)
- {
- // In 2.6.0 the 'fields' attribute has been moved from Query to QueryOQL for dependencies reasons
- ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
- }
-
- protected function AfterDBCreate(
- $sAdminUser,
- $sAdminPwd,
- $sAdminLanguage,
- $aSelectedModules
- ) {
- $aParamValues = $this->oParams->GetParamForConfigArray();
- $sTargetEnvironment = $this->GetTargetEnv();
- $sModulesDir = $this->GetTargetDir();
-
- /**
- * @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('After Database Creation');
-
- $sMode = $aParamValues['mode'];
- $oConfig = new Config();
- $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
- $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
- $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
- $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
- 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(), APPROOT.$sModulesDir);
- $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
-
- $oProductionEnv->UpdatePredefinedObjects();
-
- if ($sMode == 'install') {
- if (!self::CreateAdminAccount(MetaModel::GetConfig(), $sAdminUser, $sAdminPwd, $sAdminLanguage)) {
- throw(new Exception("Failed to create the administrator account '$sAdminUser'"));
- } else {
- SetupLog::Info("Administrator account '$sAdminUser' created.");
- }
- }
-
- // Perform final setup tasks here
- //
- $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
- }
-
- /**
- * Helper function to create and administrator account for iTop
- * @return boolean true on success, false otherwise
- */
- protected static function CreateAdminAccount(Config $oConfig, $sAdminUser, $sAdminPwd, $sLanguage)
- {
- SetupLog::Info('CreateAdminAccount');
-
- if (UserRights::CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage)) {
- return true;
- } else {
- return false;
- }
- }
-
- protected function DoLoadFiles(
- $aSelectedModules,
- $bSampleData = false
- ) {
- $aParamValues = $this->oParams->GetParamForConfigArray();
- $sTargetEnvironment = $this->GetTargetEnv();
- $sModulesDir = $this->GetTargetDir();
-
- /**
- * @since 3.2.0 move the ContextTag init at the very beginning of the method
- * @noinspection PhpUnusedLocalVariableInspection
- */
- $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
-
- $oConfig = new Config();
- $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
- $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
-
- //Load the MetaModel if needed (asynchronous mode)
- if (!self::$bMetaModelStarted) {
- $oProductionEnv->InitDataModel($oConfig, false); // load data model and connect to the database
-
- self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
- }
-
- $aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
- $oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
-
- // Perform after dbload setup tasks here
- //
- $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
- }
-
- /**
- * @param string $sPreviousConfigFile
- * @param string $sDataModelVersion
- * @param array $aSelectedModuleCodes
- * @param array $aSelectedExtensionCodes
- * @param string|null $sInstallComment
- *
- * @param null $sInstallComment
- *
- * @throws \ConfigException
- * @throws \CoreException
- * @throws \Exception
- */
- protected function DoCreateConfig(
- $sPreviousConfigFile,
- $sDataModelVersion,
- $aSelectedModuleCodes,
- $aSelectedExtensionCodes,
- $sInstallComment = null
- ) {
- $aParamValues = $this->oParams->GetParamForConfigArray();
- $sTargetEnvironment = $this->GetTargetEnv();
- $sModulesDir = $this->GetTargetDir();
-
- /**
- * @since 3.2.0 move the ContextTag init at the very beginning of the method
- * @noinspection PhpUnusedLocalVariableInspection
- */
- $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
-
- $aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
- $sMode = $aParamValues['mode'];
-
- if ($sMode == 'upgrade') {
- try {
- $oOldConfig = new Config($sPreviousConfigFile);
- $oConfig = clone($oOldConfig);
- } catch (Exception $e) {
- // In case the previous configuration is corrupted... start with a blank new one
- $oConfig = new Config();
- }
- } else {
- $oConfig = new Config();
- // To preserve backward compatibility while upgrading to 2.0.3 (when tracking_level_linked_set_default has been introduced)
- // the default value on upgrade differs from the default value at first install
- $oConfig->Set('tracking_level_linked_set_default', LINKSET_TRACKING_NONE, 'first_install');
- }
-
- $oConfig->Set('access_mode', ACCESS_FULL);
- // Final config update: add the modules
- $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
-
- // Record which modules are installed...
- $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
- $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
-
- if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sInstallComment)) {
- throw new Exception("Failed to record the installation information");
- }
-
- // Make sure the root configuration directory exists
- if (!file_exists(APPCONF)) {
- mkdir(APPCONF);
- chmod(APPCONF, 0770); // RWX for owner and group, nothing for others
- SetupLog::Info("Created configuration directory: ".APPCONF);
- }
-
- // Write the final configuration file
- $sConfigFile = APPCONF.(($sTargetEnvironment == '') ? 'production' : $sTargetEnvironment).'/'.ITOP_CONFIG_FILE;
- $sConfigDir = dirname($sConfigFile);
- @mkdir($sConfigDir);
- @chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
-
- $oConfig->WriteToFile($sConfigFile);
-
- // try to make the final config file read-only
- @chmod($sConfigFile, 0440); // Read-only for owner and group, nothing for others
-
- // Ready to go !!
- require_once(APPROOT.'core/dict.class.inc.php');
- MetaModel::ResetAllCaches();
- }
-}
-
-class SetupDBBackup extends DBBackup
-{
- protected function LogInfo($sMsg)
- {
- SetupLog::Ok('Info - '.$sMsg);
- }
-
- protected function LogError($sMsg)
- {
- SetupLog::Ok('Error - '.$sMsg);
- }
-}
-
-class DataAuditSequencer extends ApplicationInstallSequencer
-{
- public const DATA_AUDIT_FAILED = 100;
-
- protected function GetTempEnv()
- {
- $sTargetEnv = $this->GetTargetEnv();
- return 'dry-'.$sTargetEnv;
- }
-
- protected function GetTargetDir()
- {
- $sTargetEnv = $this->GetTempEnv();
- return 'env-'.$sTargetEnv;
- }
-
- /**
- * Executes the next step of the installation and reports about the progress
- * and the next step to perform
- *
- * @param string $sStep The identifier of the step to execute
- * @param string|null $sInstallComment
- *
- * @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
- */
- public function ExecuteStep($sStep = '', $sInstallComment = null)
- {
- try {
- $fStart = microtime(true);
- SetupLog::Info("##### STEP {$sStep} start");
- $this->EnterReadOnlyMode();
- switch ($sStep) {
- case '':
- $aResult = [
- 'status' => self::OK,
- 'message' => '',
- 'percentage-completed' => 20,
- 'next-step' => 'compile',
- 'next-step-label' => 'Compiling the data model',
- ];
-
- // 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("======= Data Audit starts =======\nParameters:\n$sSafeXml\n");
-
- // Save the response file as a stand-alone file as well
- $sFileName = 'data-audit-'.date('Y-m-d');
- $index = 0;
- while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
- $index++;
- $sFileName = 'data-audit-'.date('Y-m-d').'-'.$index;
- }
- file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
-
- break;
-
- case 'compile':
- $aSelectedModules = $this->oParams->Get('selected_modules');
- $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
- $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
- $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
-
- $this->DoCompile(
- $aRemovedExtensionCodes,
- $aSelectedModules,
- $sSourceDir,
- $sExtensionDir,
- false
- );
-
- $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(),
- ];
-
- SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
- $idx = 0;
- // Log the call stack, but not the parameters since they may contain passwords or other sensitive data
- SetupLog::Ok("Call stack:");
- foreach ($e->getTrace() as $aTrace) {
- $sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
- $sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
- $sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
- $sType = empty($aTrace['type']) ? "" : $aTrace['type'];
- $sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
- $sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
- SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
- $idx++;
- }
- $this->ExitReadOnlyMode();
- } finally {
- $fDuration = round(microtime(true) - $fStart, 2);
- SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
- }
-
- return $aResult;
- }
-
- protected function DoWriteConfig()
- {
- $sConfigFilePath = utils::GetConfigFilePath($this->GetTargetEnv());
- if (is_file($sConfigFilePath)) {
- $oConfig = new Config($sConfigFilePath);
-
- $sTempConfigFileName = utils::GetConfigFilePath($this->GetTempEnv());
- $sConfigDir = dirname($sTempConfigFileName);
- @mkdir($sConfigDir);
- @chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
-
- 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);
-
- $sTargetEnvironment = $this->GetTempEnv();
- $sPreviousEnvironment = $this->GetTargetEnv();
- $oSetupAudit = new SetupAudit($sPreviousEnvironment, $sTargetEnvironment);
-
- $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()
- {
- $sDestination = APPROOT.$this->GetTargetDir();
- SetupUtils::tidydir($sDestination);
- SetupUtils::rmdir_safe($sDestination);
- }
-}
+require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
/**
* For compatibility with older scripts
diff --git a/setup/sequencers/ApplicationInstallSequencer.php b/setup/sequencers/ApplicationInstallSequencer.php
new file mode 100644
index 000000000..9809e934a
--- /dev/null
+++ b/setup/sequencers/ApplicationInstallSequencer.php
@@ -0,0 +1,972 @@
+oParams = $oParams;
+
+ $aParamValues = $oParams->GetParamForConfigArray();
+ $this->oConfig = new Config();
+ $this->oConfig->UpdateFromParams($aParamValues);
+ utils::SetConfig($this->oConfig);
+ }
+
+ /**
+ * @return string
+ */
+ protected function GetTargetEnv()
+ {
+ $sTargetEnvironment = $this->oParams->Get('target_env', '');
+ if ($sTargetEnvironment !== '') {
+ return $sTargetEnvironment;
+ }
+
+ return 'production';
+ }
+
+ /**
+ * @return string
+ */
+ protected function GetTargetDir()
+ {
+ $sTargetEnv = $this->GetTargetEnv();
+ return 'env-'.$sTargetEnv;
+ }
+
+ protected function GetConfig()
+ {
+ $sTargetEnvironment = $this->GetTargetEnv();
+ $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE;
+ try {
+ $oConfig = new Config($sConfigFile);
+ } catch (Exception $e) {
+ return null;
+ }
+
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ $oConfig->UpdateFromParams($aParamValues);
+
+ return $oConfig;
+ }
+
+ /**
+ * Executes the next step of the installation and reports about the progress
+ * and the next step to perform
+ *
+ * @param string $sStep The identifier of the step to execute
+ * @param string|null $sInstallComment
+ *
+ * @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
+ */
+ public function ExecuteStep($sStep = '', $sInstallComment = null)
+ {
+ try {
+ $fStart = microtime(true);
+ SetupLog::Info("##### STEP {$sStep} start");
+ $this->EnterReadOnlyMode();
+ switch ($sStep) {
+ case '':
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'percentage-completed' => 0,
+ 'next-step' => 'copy',
+ 'next-step-label' => 'Copying data model files',
+ ];
+
+ // 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("======= Installation starts =======\nParameters:\n$sSafeXml\n");
+
+ // Save the response file as a stand-alone file as well
+ $sFileName = 'install-'.date('Y-m-d');
+ $index = 0;
+ while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
+ $index++;
+ $sFileName = 'install-'.date('Y-m-d').'-'.$index;
+ }
+ file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
+
+ break;
+
+ case 'copy':
+ $aPreinstall = $this->oParams->Get('preinstall');
+ $aCopies = $aPreinstall['copies'] ?? [];
+
+ $this->DoCopy($aCopies);
+ $sReport = "Copying...";
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => $sReport,
+ ];
+ if (isset($aPreinstall['backup'])) {
+ $aResult['next-step'] = 'backup';
+ $aResult['next-step-label'] = 'Performing a backup of the database';
+ $aResult['percentage-completed'] = 20;
+ } else {
+ $aResult['next-step'] = 'compile';
+ $aResult['next-step-label'] = 'Compiling the data model';
+ $aResult['percentage-completed'] = 20;
+ }
+ break;
+
+ case 'backup':
+ $aPreinstall = $this->oParams->Get('preinstall');
+ // __DB__-%Y-%m-%d
+ $sDestination = $aPreinstall['backup']['destination'];
+ $sSourceConfigFile = $aPreinstall['backup']['configuration_file'];
+ $sMySQLBinDir = $this->oParams->Get('mysql_bindir', null);
+ $this->DoBackup($sDestination, $sSourceConfigFile, $sMySQLBinDir);
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => "Created backup",
+ 'next-step' => 'compile',
+ 'next-step-label' => 'Compiling the data model',
+ 'percentage-completed' => 20,
+ ];
+ break;
+
+ 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', []);
+
+ $bUseSymbolicLinks = null;
+ if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
+ if (function_exists('symlink')) {
+ $bUseSymbolicLinks = true;
+ SetupLog::Info("Using symbolic links instead of copying data model files (for developers only!)");
+ } else {
+ SetupLog::Info("Symbolic links (function symlinks) does not seem to be supported on this platform (OS/PHP version).");
+ }
+ }
+
+ $this->DoCompile(
+ $aRemovedExtensionCodes,
+ $aSelectedModules,
+ $sSourceDir,
+ $sExtensionDir,
+ $bUseSymbolicLinks
+ );
+
+ $sNextStep = 'db-schema';
+ $sNextStepLabel = 'Updating database schema';
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'next-step' => $sNextStep,
+ 'next-step-label' => $sNextStepLabel,
+ 'percentage-completed' => 40,
+ ];
+ break;
+
+ case 'db-schema':
+ $aSelectedModules = $this->oParams->Get('selected_modules', []);
+
+ $this->DoUpdateDBSchema(
+ $aSelectedModules
+ );
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'next-step' => 'after-db-create',
+ 'next-step-label' => 'Creating profiles',
+ 'percentage-completed' => 60,
+ ];
+ break;
+
+ case 'after-db-create':
+ $aAdminParams = $this->oParams->Get('admin_account');
+ $sAdminUser = $aAdminParams['user'];
+ $sAdminPwd = $aAdminParams['pwd'];
+ $sAdminLanguage = $aAdminParams['language'];
+ $aSelectedModules = $this->oParams->Get('selected_modules', []);
+
+ $this->AfterDBCreate(
+ $sAdminUser,
+ $sAdminPwd,
+ $sAdminLanguage,
+ $aSelectedModules
+ );
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'next-step' => 'load-data',
+ 'next-step-label' => 'Loading data',
+ 'percentage-completed' => 80,
+ ];
+ break;
+
+ case 'load-data':
+ $aSelectedModules = $this->oParams->Get('selected_modules');
+ $bSampleData = ($this->oParams->Get('sample_data', 0) == 1);
+
+ $this->DoLoadFiles(
+ $aSelectedModules,
+ $bSampleData
+ );
+
+ $aResult = [
+ 'status' => self::INFO,
+ 'message' => 'All data loaded',
+ 'next-step' => 'create-config',
+ 'next-step-label' => 'Creating the configuration File',
+ 'percentage-completed' => 99,
+ ];
+ break;
+
+ case 'create-config':
+ $sPreviousConfigFile = $this->oParams->Get('previous_configuration_file', '');
+ $sDataModelVersion = $this->oParams->Get('datamodel_version', '0.0.0');
+ $aSelectedModuleCodes = $this->oParams->Get('selected_modules', []);
+ $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []);
+
+ $this->DoCreateConfig(
+ $sPreviousConfigFile,
+ $sDataModelVersion,
+ $aSelectedModuleCodes,
+ $aSelectedExtensionCodes,
+ $sInstallComment
+ );
+
+ $aResult = [
+ 'status' => self::INFO,
+ 'message' => 'Configuration file created',
+ 'next-step' => '',
+ 'next-step-label' => 'Completed',
+ 'percentage-completed' => 100,
+ ];
+ $this->ExitReadOnlyMode();
+ 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,
+ ];
+
+ SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
+ $idx = 0;
+ // Log the call stack, but not the parameters since they may contain passwords or other sensitive data
+ SetupLog::Ok("Call stack:");
+ foreach ($e->getTrace() as $aTrace) {
+ $sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
+ $sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
+ $sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
+ $sType = empty($aTrace['type']) ? "" : $aTrace['type'];
+ $sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
+ $sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
+ SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
+ $idx++;
+ }
+ } finally {
+ $fDuration = round(microtime(true) - $fStart, 2);
+ SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
+ }
+
+ return $aResult;
+ }
+
+ protected function EnterReadOnlyMode()
+ {
+ if ($this->GetTargetEnv() != 'production') {
+ return;
+ }
+
+ if (SetupUtils::IsInReadOnlyMode()) {
+ return;
+ }
+
+ SetupUtils::EnterReadOnlyMode($this->GetConfig());
+ }
+
+ protected function ExitReadOnlyMode()
+ {
+ if ($this->GetTargetEnv() != 'production') {
+ return;
+ }
+
+ if (!SetupUtils::IsInReadOnlyMode()) {
+ return;
+ }
+
+ 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
+ * @param string $sMySQLBinDir
+ *
+ * @throws \BackupException
+ * @throws \CoreException
+ * @throws \MySQLException
+ * @since 2.5.0 uses a {@link Config} object to store DB parameters
+ */
+ protected function DoBackup($sBackupFileFormat, $sSourceConfigFile, $sMySQLBinDir = null)
+ {
+ $oBackup = new SetupDBBackup($this->oConfig);
+ $sTargetFile = $oBackup->MakeName($sBackupFileFormat);
+ if (!empty($sMySQLBinDir)) {
+ $oBackup->SetMySQLBinDir($sMySQLBinDir);
+ }
+
+ CMDBSource::InitFromConfig($this->oConfig);
+ $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)
+ {
+ /**
+ * @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);
+ 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);
+ //$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) {
+ SetupUtils::ExitMaintenanceMode();
+ }
+ }
+
+ protected function GetModelInfoPath(string $sEnv): string
+ {
+ return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
+ }
+
+ protected function SaveModelInfo(string $sEnvironment): bool
+ {
+ $sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
+ try {
+ $aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
+ } catch (Exception $e) {
+ //logged already
+ return is_file($sModelInfoPath);
+ }
+
+ return (bool) file_put_contents($sModelInfoPath, json_encode($aModelInfo));
+ }
+
+ protected function GetPreviousModelInfo(string $sEnvironment): array
+ {
+ $sContent = file_get_contents($this->GetModelInfoPath($sEnvironment));
+ $aModelInfo = json_decode($sContent, true);
+
+ if (false === $aModelInfo) {
+ throw new Exception("Could not read (before compilation) previous model to audit data");
+ }
+
+ return $aModelInfo;
+ }
+
+ protected function IsSetupDataAuditEnabled($sSkipDataAudit, array $aParamValues): bool
+ {
+ if ($sSkipDataAudit === "checked") {
+ SetupLog::Info("Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
+ return false;
+ }
+
+ $sMode = $aParamValues['mode'];
+ if ($sMode !== "upgrade") {
+ //first install
+ return false;
+ }
+
+ $sPath = APPROOT.$this->GetTargetDir();
+ if (!is_dir($sPath)) {
+ SetupLog::Info("Reinstallation of an iTop from a backup (No ".$this->GetTargetDir()." found). Setup data audit disabled", null, ['skip-data-audit' => $sSkipDataAudit]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param $aSelectedModules
+ *
+ * @throws \ConfigException
+ * @throws \CoreException
+ * @throws \MySQLException
+ */
+ protected function DoUpdateDBSchema($aSelectedModules)
+ {
+ $sTargetEnvironment = $this->GetTargetEnv();
+ $sModulesDir = $this->GetTargetDir();
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ /**
+ * @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("Update Database Schema for environment '$sTargetEnvironment'.");
+ $sMode = $aParamValues['mode'];
+ $sDBPrefix = $aParamValues['db_prefix'];
+ $sDBName = $aParamValues['db_name'];
+
+ $oConfig = new Config();
+ $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
+
+ $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
+ $oProductionEnv->InitDataModel($oConfig, true); // load data model only
+
+ // Migrate columns
+ self::MoveColumns($sDBPrefix);
+
+ // Migrate application data format
+ //
+ // priv_internalUser caused troubles because MySQL transforms table names to lower case under Windows
+ // This becomes an issue when moving your installation data to/from Windows
+ // Starting 2.0, all table names must be lowercase
+ if ($sMode != 'install') {
+ SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' into '{$sDBPrefix}priv_internaluser' (lowercase)");
+ // This command will have no effect under Windows...
+ // and it has been written in two steps so as to make it work under windows!
+ CMDBSource::SelectDB($sDBName);
+ try {
+ $sRepair = "RENAME TABLE `{$sDBPrefix}priv_internalUser` TO `{$sDBPrefix}priv_internaluser_other`, `{$sDBPrefix}priv_internaluser_other` TO `{$sDBPrefix}priv_internaluser`";
+ CMDBSource::Query($sRepair);
+ } catch (Exception $e) {
+ SetupLog::Info("Renaming '{$sDBPrefix}priv_internalUser' failed (already done in a previous upgrade?)");
+ }
+
+ // let's remove the records in priv_change which have no counterpart in priv_changeop
+ SetupLog::Info("Cleanup of '{$sDBPrefix}priv_change' to remove orphan records");
+ CMDBSource::SelectDB($sDBName);
+ try {
+ $sTotalCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change`";
+ $iTotalCount = (int)CMDBSource::QueryToScalar($sTotalCount);
+ SetupLog::Info("There is a total of $iTotalCount records in {$sDBPrefix}priv_change.");
+
+ $sOrphanCount = "SELECT COUNT(c.id) FROM `{$sDBPrefix}priv_change` AS c left join `{$sDBPrefix}priv_changeop` AS o ON c.id = o.changeid WHERE o.id IS NULL";
+ $iOrphanCount = (int)CMDBSource::QueryToScalar($sOrphanCount);
+ SetupLog::Info("There are $iOrphanCount useless records in {$sDBPrefix}priv_change (".sprintf('%.2f', ((100.0 * $iOrphanCount) / $iTotalCount))."%)");
+ if ($iOrphanCount > 0) {
+ //N°3793
+ if ($iOrphanCount > 100000) {
+ SetupLog::Info("There are too much useless records ($iOrphanCount) in {$sDBPrefix}priv_change. Cleanup cannot be done during setup.");
+ } else {
+ SetupLog::Info("Removing the orphan records...");
+ $sCleanup = "DELETE FROM `{$sDBPrefix}priv_change` USING `{$sDBPrefix}priv_change` LEFT JOIN `{$sDBPrefix}priv_changeop` ON `{$sDBPrefix}priv_change`.id = `{$sDBPrefix}priv_changeop`.changeid WHERE `{$sDBPrefix}priv_changeop`.id IS NULL;";
+ CMDBSource::Query($sCleanup);
+ SetupLog::Info("Cleanup completed successfully.");
+ }
+ } else {
+ SetupLog::Info("Ok, nothing to cleanup.");
+ }
+ } catch (Exception $e) {
+ SetupLog::Info("Cleanup of orphan records in `{$sDBPrefix}priv_change` failed: ".$e->getMessage());
+ }
+
+ }
+
+ // Module specific actions (migrate the data)
+ $aAvailableModules = $oProductionEnv->AnalyzeInstallation(MetaModel::GetConfig(), APPROOT.$sModulesDir);
+ $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation', $aSelectedModules);
+
+ if (!$oProductionEnv->CreateDatabaseStructure(MetaModel::GetConfig(), $sMode)) {
+ throw new Exception("Failed to create/upgrade the database structure for environment '$sTargetEnvironment'");
+ }
+
+ // Set a DBProperty with a unique ID to identify this instance of iTop
+ $sUUID = DBProperty::GetProperty('database_uuid', '');
+ if ($sUUID === '') {
+ $sUUID = utils::CreateUUID('database');
+ DBProperty::SetProperty('database_uuid', $sUUID, 'Installation/upgrade of '.ITOP_APPLICATION, 'Unique ID of this '.ITOP_APPLICATION.' Database');
+ }
+
+ // priv_change now has an 'origin' field to distinguish between the various input sources
+ // Let's initialize the field with 'interactive' for all records were it's null
+ // Then check if some records should hold a different value, based on a pattern matching in the userinfo field
+ CMDBSource::SelectDB($sDBName);
+ try {
+ $sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_change` WHERE `origin` IS NULL";
+ $iCount = (int)CMDBSource::QueryToScalar($sCount);
+ if ($iCount > 0) {
+ SetupLog::Info("Initializing '{$sDBPrefix}priv_change.origin' ($iCount records to update)");
+
+ // By default all uninitialized values are considered as 'interactive'
+ $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'interactive' WHERE `origin` IS NULL";
+ CMDBSource::Query($sInit);
+
+ // CSV Import was identified by the comment at the end
+ $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-import.php' WHERE `userinfo` LIKE '%Web Service (CSV)'";
+ CMDBSource::Query($sInit);
+
+ // CSV Import was identified by the comment at the end
+ $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'csv-interactive' WHERE `userinfo` LIKE '%(CSV)' AND origin = 'interactive'";
+ CMDBSource::Query($sInit);
+
+ // Syncho data sources were identified by the comment at the end
+ // Unfortunately the comment is localized, so we have to search for all possible patterns
+ $sCurrentLanguage = Dict::GetUserLanguage();
+ $aSuffixes = [];
+ foreach (array_keys(Dict::GetLanguages()) as $sLangCode) {
+ Dict::SetUserLanguage($sLangCode);
+ $sSuffix = CMDBSource::Quote('%'.Dict::S('Core:SyncDataExchangeComment'));
+ $aSuffixes[$sSuffix] = true;
+ }
+ Dict::SetUserLanguage($sCurrentLanguage);
+ $sCondition = "`userinfo` LIKE ".implode(" OR `userinfo` LIKE ", array_keys($aSuffixes));
+
+ $sInit = "UPDATE `{$sDBPrefix}priv_change` SET `origin` = 'synchro-data-source' WHERE ($sCondition)";
+ CMDBSource::Query($sInit);
+
+ SetupLog::Info("Initialization of '{$sDBPrefix}priv_change.origin' completed.");
+ } else {
+ SetupLog::Info("'{$sDBPrefix}priv_change.origin' already initialized, nothing to do.");
+ }
+ } catch (Exception $e) {
+ SetupLog::Error("Initializing '{$sDBPrefix}priv_change.origin' failed: ".$e->getMessage());
+ }
+
+ // priv_async_task now has a 'status' field to distinguish between the various statuses rather than just relying on the date columns
+ // Let's initialize the field with 'planned' or 'error' for all records were it's null
+ CMDBSource::SelectDB($sDBName);
+ try {
+ $sCount = "SELECT COUNT(*) FROM `{$sDBPrefix}priv_async_task` WHERE `status` IS NULL";
+ $iCount = (int)CMDBSource::QueryToScalar($sCount);
+ if ($iCount > 0) {
+ SetupLog::Info("Initializing '{$sDBPrefix}priv_async_task.status' ($iCount records to update)");
+
+ $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'planned' WHERE (`status` IS NULL) AND (`started` IS NULL)";
+ CMDBSource::Query($sInit);
+
+ $sInit = "UPDATE `{$sDBPrefix}priv_async_task` SET `status` = 'error' WHERE (`status` IS NULL) AND (`started` IS NOT NULL)";
+ CMDBSource::Query($sInit);
+
+ SetupLog::Info("Initialization of '{$sDBPrefix}priv_async_task.status' completed.");
+ } else {
+ SetupLog::Info("'{$sDBPrefix}priv_async_task.status' already initialized, nothing to do.");
+ }
+ } catch (Exception $e) {
+ SetupLog::Error("Initializing '{$sDBPrefix}priv_async_task.status' failed: ".$e->getMessage());
+ }
+
+ SetupLog::Info("Database Schema Successfully Updated for environment '$sTargetEnvironment'.");
+ }
+
+ /**
+ * @param string $sDBPrefix
+ *
+ * @throws \CoreException
+ * @throws \MySQLException
+ */
+ protected static function MoveColumns($sDBPrefix)
+ {
+ // In 2.6.0 the 'fields' attribute has been moved from Query to QueryOQL for dependencies reasons
+ ModuleInstallerAPI::MoveColumnInDB($sDBPrefix.'priv_query', 'fields', $sDBPrefix.'priv_query_oql', 'fields');
+ }
+
+ protected function AfterDBCreate(
+ $sAdminUser,
+ $sAdminPwd,
+ $sAdminLanguage,
+ $aSelectedModules
+ ) {
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ $sTargetEnvironment = $this->GetTargetEnv();
+ $sModulesDir = $this->GetTargetDir();
+
+ /**
+ * @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('After Database Creation');
+
+ $sMode = $aParamValues['mode'];
+ $oConfig = new Config();
+ $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
+
+ $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
+ $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
+ $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
+ 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(), APPROOT.$sModulesDir);
+ $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation', $aSelectedModules);
+
+ $oProductionEnv->UpdatePredefinedObjects();
+
+ if ($sMode == 'install') {
+ if (!self::CreateAdminAccount(MetaModel::GetConfig(), $sAdminUser, $sAdminPwd, $sAdminLanguage)) {
+ throw(new Exception("Failed to create the administrator account '$sAdminUser'"));
+ } else {
+ SetupLog::Info("Administrator account '$sAdminUser' created.");
+ }
+ }
+
+ // Perform final setup tasks here
+ //
+ $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup', $aSelectedModules);
+ }
+
+ /**
+ * Helper function to create and administrator account for iTop
+ * @return boolean true on success, false otherwise
+ */
+ protected static function CreateAdminAccount(Config $oConfig, $sAdminUser, $sAdminPwd, $sLanguage)
+ {
+ SetupLog::Info('CreateAdminAccount');
+
+ if (UserRights::CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function DoLoadFiles(
+ $aSelectedModules,
+ $bSampleData = false
+ ) {
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ $sTargetEnvironment = $this->GetTargetEnv();
+ $sModulesDir = $this->GetTargetDir();
+
+ /**
+ * @since 3.2.0 move the ContextTag init at the very beginning of the method
+ * @noinspection PhpUnusedLocalVariableInspection
+ */
+ $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
+
+ $oConfig = new Config();
+ $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
+
+ $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
+
+ //Load the MetaModel if needed (asynchronous mode)
+ if (!self::$bMetaModelStarted) {
+ $oProductionEnv->InitDataModel($oConfig, false); // load data model and connect to the database
+
+ self::$bMetaModelStarted = true; // No need to reload the final MetaModel in case the installer runs synchronously
+ }
+
+ $aAvailableModules = $oProductionEnv->AnalyzeInstallation($oConfig, APPROOT.$sModulesDir);
+ $oProductionEnv->LoadData($aAvailableModules, $bSampleData, $aSelectedModules);
+
+ // Perform after dbload setup tasks here
+ //
+ $oProductionEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad', $aSelectedModules);
+ }
+
+ /**
+ * @param string $sPreviousConfigFile
+ * @param string $sDataModelVersion
+ * @param array $aSelectedModuleCodes
+ * @param array $aSelectedExtensionCodes
+ * @param string|null $sInstallComment
+ *
+ * @param null $sInstallComment
+ *
+ * @throws \ConfigException
+ * @throws \CoreException
+ * @throws \Exception
+ */
+ protected function DoCreateConfig(
+ $sPreviousConfigFile,
+ $sDataModelVersion,
+ $aSelectedModuleCodes,
+ $aSelectedExtensionCodes,
+ $sInstallComment = null
+ ) {
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ $sTargetEnvironment = $this->GetTargetEnv();
+ $sModulesDir = $this->GetTargetDir();
+
+ /**
+ * @since 3.2.0 move the ContextTag init at the very beginning of the method
+ * @noinspection PhpUnusedLocalVariableInspection
+ */
+ $oContextTag = new ContextTag(ContextTag::TAG_SETUP);
+
+ $aParamValues['selected_modules'] = implode(',', $aSelectedModuleCodes);
+ $sMode = $aParamValues['mode'];
+
+ if ($sMode == 'upgrade') {
+ try {
+ $oOldConfig = new Config($sPreviousConfigFile);
+ $oConfig = clone($oOldConfig);
+ } catch (Exception $e) {
+ // In case the previous configuration is corrupted... start with a blank new one
+ $oConfig = new Config();
+ }
+ } else {
+ $oConfig = new Config();
+ // To preserve backward compatibility while upgrading to 2.0.3 (when tracking_level_linked_set_default has been introduced)
+ // the default value on upgrade differs from the default value at first install
+ $oConfig->Set('tracking_level_linked_set_default', LINKSET_TRACKING_NONE, 'first_install');
+ }
+
+ $oConfig->Set('access_mode', ACCESS_FULL);
+ // Final config update: add the modules
+ $oConfig->UpdateFromParams($aParamValues, $sModulesDir);
+
+ // Record which modules are installed...
+ $oProductionEnv = new RunTimeEnvironment($sTargetEnvironment);
+ $oProductionEnv->InitDataModel($oConfig, true); // load data model and connect to the database
+
+ if (!$oProductionEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModuleCodes, $aSelectedExtensionCodes, $sInstallComment)) {
+ throw new Exception("Failed to record the installation information");
+ }
+
+ // Make sure the root configuration directory exists
+ if (!file_exists(APPCONF)) {
+ mkdir(APPCONF);
+ chmod(APPCONF, 0770); // RWX for owner and group, nothing for others
+ SetupLog::Info("Created configuration directory: ".APPCONF);
+ }
+
+ // Write the final configuration file
+ $sConfigFile = APPCONF.(($sTargetEnvironment == '') ? 'production' : $sTargetEnvironment).'/'.ITOP_CONFIG_FILE;
+ $sConfigDir = dirname($sConfigFile);
+ @mkdir($sConfigDir);
+ @chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
+
+ $oConfig->WriteToFile($sConfigFile);
+
+ // try to make the final config file read-only
+ @chmod($sConfigFile, 0440); // Read-only for owner and group, nothing for others
+
+ // Ready to go !!
+ require_once(APPROOT.'core/dict.class.inc.php');
+ MetaModel::ResetAllCaches();
+ }
+}
diff --git a/setup/sequencers/DataAuditSequencer.php b/setup/sequencers/DataAuditSequencer.php
new file mode 100644
index 000000000..15edc27bb
--- /dev/null
+++ b/setup/sequencers/DataAuditSequencer.php
@@ -0,0 +1,232 @@
+GetTargetEnv();
+ return 'dry-'.$sTargetEnv;
+ }
+
+ protected function GetTargetDir()
+ {
+ $sTargetEnv = $this->GetTempEnv();
+ return 'env-'.$sTargetEnv;
+ }
+
+ /**
+ * Executes the next step of the installation and reports about the progress
+ * and the next step to perform
+ *
+ * @param string $sStep The identifier of the step to execute
+ * @param string|null $sInstallComment
+ *
+ * @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
+ */
+ public function ExecuteStep($sStep = '', $sInstallComment = null)
+ {
+ try {
+ $fStart = microtime(true);
+ SetupLog::Info("##### STEP {$sStep} start");
+ $this->EnterReadOnlyMode();
+ switch ($sStep) {
+ case '':
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'percentage-completed' => 20,
+ 'next-step' => 'compile',
+ 'next-step-label' => 'Compiling the data model',
+ ];
+
+ // 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("======= Data Audit starts =======\nParameters:\n$sSafeXml\n");
+
+ // Save the response file as a stand-alone file as well
+ $sFileName = 'data-audit-'.date('Y-m-d');
+ $index = 0;
+ while (file_exists(APPROOT.'log/'.$sFileName.'.xml')) {
+ $index++;
+ $sFileName = 'data-audit-'.date('Y-m-d').'-'.$index;
+ }
+ file_put_contents(APPROOT.'log/'.$sFileName.'.xml', $sSafeXml);
+
+ break;
+
+ case 'compile':
+ $aSelectedModules = $this->oParams->Get('selected_modules');
+ $sSourceDir = $this->oParams->Get('source_dir', 'datamodels/latest');
+ $sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
+ $aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
+
+ $this->DoCompile(
+ $aRemovedExtensionCodes,
+ $aSelectedModules,
+ $sSourceDir,
+ $sExtensionDir,
+ false
+ );
+
+ $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(),
+ ];
+
+ SetupLog::Error('An exception occurred: '.$e->getMessage().' at line '.$e->getLine().' in file '.$e->getFile());
+ $idx = 0;
+ // Log the call stack, but not the parameters since they may contain passwords or other sensitive data
+ SetupLog::Ok("Call stack:");
+ foreach ($e->getTrace() as $aTrace) {
+ $sLine = empty($aTrace['line']) ? "" : $aTrace['line'];
+ $sFile = empty($aTrace['file']) ? "" : $aTrace['file'];
+ $sClass = empty($aTrace['class']) ? "" : $aTrace['class'];
+ $sType = empty($aTrace['type']) ? "" : $aTrace['type'];
+ $sFunction = empty($aTrace['function']) ? "" : $aTrace['function'];
+ $sVerb = empty($sClass) ? $sFunction : "$sClass{$sType}$sFunction";
+ SetupLog::Ok("#$idx $sFile($sLine): $sVerb(...)");
+ $idx++;
+ }
+ $this->ExitReadOnlyMode();
+ } finally {
+ $fDuration = round(microtime(true) - $fStart, 2);
+ SetupLog::Info("##### STEP {$sStep} duration: {$fDuration}s");
+ }
+
+ return $aResult;
+ }
+
+ protected function DoWriteConfig()
+ {
+ $sConfigFilePath = utils::GetConfigFilePath($this->GetTargetEnv());
+ if (is_file($sConfigFilePath)) {
+ $oConfig = new Config($sConfigFilePath);
+
+ $sTempConfigFileName = utils::GetConfigFilePath($this->GetTempEnv());
+ $sConfigDir = dirname($sTempConfigFileName);
+ @mkdir($sConfigDir);
+ @chmod($sConfigDir, 0770); // RWX for owner and group, nothing for others
+
+ 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);
+
+ $sTargetEnvironment = $this->GetTempEnv();
+ $sPreviousEnvironment = $this->GetTargetEnv();
+
+ $oSetupAudit = new SetupAudit($sPreviousEnvironment, $sTargetEnvironment);
+
+ //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()
+ {
+ $sDestination = APPROOT.$this->GetTargetDir();
+ SetupUtils::tidydir($sDestination);
+ SetupUtils::rmdir_safe($sDestination);
+ }
+}
diff --git a/setup/StepSequencer.php b/setup/sequencers/StepSequencer.php
similarity index 74%
rename from setup/StepSequencer.php
rename to setup/sequencers/StepSequencer.php
index a29cb2f56..e2036660d 100644
--- a/setup/StepSequencer.php
+++ b/setup/sequencers/StepSequencer.php
@@ -1,5 +1,22 @@
sInitialStepClass);
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
$oStep = new $sCurrentStepClass($this, $sCurrentState);
- $aNextStepInfo = $oStep->ProcessParams(false); // false => Moving backwards
+ $aNextStepInfo = $oStep->UpdateWizardStateAndGetNextStep(false); // false => Moving backwards
// Display the previous step
$aCurrentStepInfo = $this->PopStep();
diff --git a/setup/wizardsteps/WizStepDataAudit.php b/setup/wizardsteps/WizStepDataAudit.php
index f916000a4..b71b051f8 100644
--- a/setup/wizardsteps/WizStepDataAudit.php
+++ b/setup/wizardsteps/WizStepDataAudit.php
@@ -19,6 +19,8 @@
*/
use Combodo\iTop\Application\WebPage\WebPage;
+require_once(APPROOT.'setup/sequencers/DataAuditSequencer.php');
+
/**
* @since 3.3.0
*/
@@ -74,7 +76,7 @@ class WizStepDataAudit extends WizStepInstall
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('');
- $sApplicationUrl = $this->oWizard->GetParameter('application_url').'pages/UI.php';
+ $sApplicationUrl = $this->oWizard->GetParameter('application_url').'pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php';
$oPage->add('');
if (!$this->CheckDependencies()) {
diff --git a/setup/wizardsteps/WizStepInstall.php b/setup/wizardsteps/WizStepInstall.php
index c01ad78cc..1bb66f399 100644
--- a/setup/wizardsteps/WizStepInstall.php
+++ b/setup/wizardsteps/WizStepInstall.php
@@ -19,6 +19,8 @@
*/
use Combodo\iTop\Application\WebPage\WebPage;
+require_once(APPROOT.'setup/sequencers/ApplicationInstallSequencer.php');
+
class WizStepInstall extends AbstractWizStepInstall
{
public const SequencerClass = ApplicationInstallSequencer::class;