diff --git a/setup/AnalyzeInstallation.php b/setup/AnalyzeInstallation.php
index 1335c9aaf..a1947b7df 100644
--- a/setup/AnalyzeInstallation.php
+++ b/setup/AnalyzeInstallation.php
@@ -6,7 +6,6 @@ class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
- private ?array $aSelectInstall = null;
protected function __construct()
{
@@ -23,7 +22,7 @@ class AnalyzeInstallation
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
- static::$oInstance = $oInstance;
+ self::$oInstance = $oInstance;
}
/**
diff --git a/setup/ModuleInstallationRepository.php b/setup/ModuleInstallationRepository.php
index f98011dd5..149899171 100644
--- a/setup/ModuleInstallationRepository.php
+++ b/setup/ModuleInstallationRepository.php
@@ -19,7 +19,7 @@ class ModuleInstallationRepository
final public static function SetInstance(?ModuleInstallationRepository $oInstance): void
{
- static::$oInstance = $oInstance;
+ self::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
@@ -95,8 +95,17 @@ SQL;
$aSelectInstall = CMDBSource::QueryToArray($sSQLQuery);
} 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());
+ SetupLog::Error(
+ 'Can not connect to the database',
+ null,
+ [
+ 'host' => $oConfig->Get('db_host'),
+ 'user' => $oConfig->Get('db_user'),
+ 'pwd:' => $oConfig->Get('db_pwd'),
+ 'db name' => $oConfig->Get('db_name'),
+ 'msg' => $e->getMessage(),
+ ]
+ );
return false;
}
@@ -129,8 +138,10 @@ SQL;
// so assume that the datamodel version is equal to the application version
$aResult['datamodel_version'] = $aResult['product_version'];
}
- $this->log_info("GetApplicationVersion returns: product_name: ".$aResult['product_name'].', product_version: '.$aResult['product_version']);
- return empty($aResult) ? false : $aResult;
+
+ SetupLog::Info(__METHOD__, null, ["product_name" => $aResult['product_name'], "product_version" => $aResult['product_version']]);
+
+ return count($aResult) == 0 ? false : $aResult;
}
private function ComputeInstalledModules(array $aSelectInstall): array
@@ -194,8 +205,8 @@ SQL;
$oSet->SetLimit($iOffset + 1);
$iParentId = 0;
- /** @var \DBObject $oModuleInstallation */
- while ($oModuleInstallation = $oSet->Fetch()) {
+ while (!is_null($oModuleInstallation = $oSet->Fetch())) {
+ /** @var \DBObject $oModuleInstallation */
if ($iOffset == 0) {
$iParentId = $oModuleInstallation->Get('id');
break;
diff --git a/setup/ajax.dataloader.php b/setup/ajax.dataloader.php
index 3e9f498c1..50dae5701 100644
--- a/setup/ajax.dataloader.php
+++ b/setup/ajax.dataloader.php
@@ -127,7 +127,7 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
/**
* Main program
*/
-$sOperation = Utils::ReadParam('operation', '');
+$sOperation = utils::ReadParam('operation', '');
try {
SetupUtils::CheckSetupToken();
@@ -164,7 +164,7 @@ try {
break;
case 'toggle_use_symbolic_links':
- $sUseSymbolicLinks = Utils::ReadParam('bUseSymbolicLinks', false);
+ $sUseSymbolicLinks = utils::ReadParam('bUseSymbolicLinks', false);
$bUseSymbolicLinks = ($sUseSymbolicLinks === 'true');
MFCompiler::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
echo "toggle useSymbolicLinks flag : $bUseSymbolicLinks";
diff --git a/setup/applicationinstaller.class.inc.php b/setup/applicationinstaller.class.inc.php
index 377998b05..5e05767bd 100644
--- a/setup/applicationinstaller.class.inc.php
+++ b/setup/applicationinstaller.class.inc.php
@@ -17,9 +17,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see
+use Combodo\iTop\Setup\FeatureRemoval\InplaceSetupAudit;
+use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer;
+
require_once(APPROOT.'setup/parameters.class.inc.php');
require_once(APPROOT.'setup/xmldataloader.class.inc.php');
require_once(APPROOT.'setup/backup.class.inc.php');
+require_once APPROOT.'setup/feature_removal/InplaceSetupAudit.php';
/**
* The base class for the installation process.
@@ -258,6 +262,7 @@ class ApplicationInstaller
$sExtensionDir = $this->oParams->Get('extensions_dir', 'extensions');
$aMiscOptions = $this->oParams->Get('options', []);
$aRemovedExtensionCodes = $this->oParams->Get('removed_extensions', []);
+ $sSkipDataAudit = $this->oParams->Get('skip-data-audit', '');
$bUseSymbolicLinks = null;
if ((isset($aMiscOptions['symlinks']) && $aMiscOptions['symlinks'])) {
@@ -269,34 +274,50 @@ class ApplicationInstaller
}
}
+ $aParamValues = $this->oParams->GetParamForConfigArray();
+ $bIsSetupDataAuditEnabled = $this->IsSetupDataAuditEnabled($sSkipDataAudit, $aParamValues);
$this->DoCompile(
$aRemovedExtensionCodes,
$aSelectedModules,
$sSourceDir,
$sExtensionDir,
+ $bIsSetupDataAuditEnabled,
$bUseSymbolicLinks
);
+ if ($bIsSetupDataAuditEnabled) {
+ $sNextStep = 'setup-audit';
+ $sNextStepLabel = 'Checking data consistency with the new data model';
+ } else {
+ $sNextStep = 'db-schema';
+ $sNextStepLabel = 'Updating database schema';
+ }
+
+ $aResult = [
+ 'status' => self::OK,
+ 'message' => '',
+ 'next-step' => $sNextStep,
+ 'next-step-label' => $sNextStepLabel,
+ 'percentage-completed' => 40,
+ ];
+ break;
+
+ case 'setup-audit':
+ $this->DoSetupAudit();
$aResult = [
'status' => self::OK,
'message' => '',
'next-step' => 'db-schema',
'next-step-label' => 'Updating database schema',
- 'percentage-completed' => 40,
+ 'percentage-completed' => 50,
];
break;
case 'db-schema':
$aSelectedModules = $this->oParams->Get('selected_modules', []);
- $aParamValues = $this->oParams->GetParamForConfigArray();
- $bOldAddon = $this->oParams->Get('old_addon', false);
- $sUrl = $this->oParams->Get('url', '');
$this->DoUpdateDBSchema(
- $aSelectedModules,
- $aParamValues,
- $bOldAddon,
- $sUrl
+ $aSelectedModules
);
$aResult = [
@@ -487,6 +508,7 @@ class ApplicationInstaller
* @param array $aSelectedModules
* @param string $sSourceDir
* @param string $sExtensionDir
+ * @param bool $bIsSetupDataAuditEnabled
* @param boolean $bUseSymbolicLinks
*
* @return void
@@ -495,8 +517,14 @@ class ApplicationInstaller
*
* @since 3.1.0 N°2013 added the aParamValues param
*/
- protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bUseSymbolicLinks = null)
+ protected function DoCompile($aRemovedExtensionCodes, $aSelectedModules, $sSourceDir, $sExtensionDir, $bIsSetupDataAuditEnabled, $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');
@@ -528,7 +556,18 @@ class ApplicationInstaller
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 ($bIsSetupDataAuditEnabled) {
+ if ($bIsAlreadyInMaintenanceMode) {
+ //required to read DM before calling SaveModelInfo
+ SetupUtils::ExitMaintenanceMode();
+ $bIsAlreadyInMaintenanceMode = false;
+ }
+
+ $this->SaveModelInfo($sEnvironment);
+ }
+
if (($sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) {
$sConfigFilePath = utils::GetConfigFilePath($sEnvironment);
if (is_file($sConfigFilePath)) {
@@ -620,23 +659,79 @@ class ApplicationInstaller
}
}
+ private function GetModelInfoPath(string $sEnv): string
+ {
+ return APPROOT."data/beforecompilation_".$sEnv."_modelinfo.json";
+ }
+
+ private function SaveModelInfo(string $sEnvironment): void
+ {
+ $aModelInfo = ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($sEnvironment);
+ $sModelInfoPath = $this->GetModelInfoPath($sEnvironment);
+ file_put_contents($sModelInfoPath, json_encode($aModelInfo));
+ }
+
+ private 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 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->GetTargetEnv();
+ $aPreviousCompilationModelInfo = $this->GetPreviousModelInfo($sTargetEnvironment);
+
+ $oSetupAudit = new InplaceSetupAudit($aPreviousCompilationModelInfo, $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");
+ }
+ }
+
+ private 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;
+ }
+
+ return true;
+ }
+
/**
* @param $aSelectedModules
- * @param $sModulesDir
- * @param $aParamValues
- * @param string $sTargetEnvironment
- * @param bool $bOldAddon
- * @param string $sAppRootUrl
*
* @throws \ConfigException
* @throws \CoreException
* @throws \MySQLException
*/
- protected function DoUpdateDBSchema($aSelectedModules, $aParamValues, $bOldAddon = false, $sAppRootUrl = '')
+ 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
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index 38a86d45c..edbc37214 100644
--- a/setup/compiler.class.inc.php
+++ b/setup/compiler.class.inc.php
@@ -21,8 +21,8 @@
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\Page;
-use Combodo\iTop\DesignElement;
use Combodo\iTop\DesignDocument;
+use Combodo\iTop\DesignElement;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/modelfactory.class.inc.php');
@@ -3359,6 +3359,8 @@ EOF;
$bDataXmlPrecompiledFileExists = false;
clearstatcache();
+
+ $iDataXmlFileLastModified = 0;
if (!empty($sPrecompiledFileUri)) {
$sDataXmlProvidedPrecompiledFile = $sTempTargetDir.DIRECTORY_SEPARATOR.$sPrecompiledFileUri;
$bDataXmlPrecompiledFileExists = file_exists($sDataXmlProvidedPrecompiledFile) ;
diff --git a/setup/email.test.php b/setup/email.test.php
index 184a2124a..e5df5a275 100644
--- a/setup/email.test.php
+++ b/setup/email.test.php
@@ -34,7 +34,7 @@ require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin)
-$sOperation = Utils::ReadParam('operation', 'step1');
+$sOperation = utils::ReadParam('operation', 'step1');
$oP = new SetupPage('iTop email test utility');
// Although this page doesn't expose sensitive info, with it we can send multiple emails
@@ -208,7 +208,7 @@ function DisplayStep2(SetupPage $oP, $sFrom, $sTo)
$oP->add("
Sending an email to '".htmlentities($sTo, ENT_QUOTES, 'utf-8')."'... (From: '".htmlentities($sFrom, ENT_QUOTES, 'utf-8')."')
\n");
$oP->add("