diff --git a/datamodels/2.x/combodo-data-feature-removal/dictionaries/en.dict.combodo-data-feature-removal.php b/datamodels/2.x/combodo-data-feature-removal/dictionaries/en.dict.combodo-data-feature-removal.php index e3f162a2a3..6671932f97 100644 --- a/datamodels/2.x/combodo-data-feature-removal/dictionaries/en.dict.combodo-data-feature-removal.php +++ b/datamodels/2.x/combodo-data-feature-removal/dictionaries/en.dict.combodo-data-feature-removal.php @@ -21,6 +21,7 @@ Dict::Add('EN US', 'English', 'English', [ 'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.', 'DataFeatureRemoval:Features:Title' => 'Features', + 'DataFeatureRemoval:Execution:Title' => 'Deletion Executions', 'DataFeatureRemoval:Analysis:Title' => 'Analysis result', 'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing', @@ -35,6 +36,9 @@ Dict::Add('EN US', 'English', 'English', [ 'DataFeatureRemoval:Table:Analysis:Module' => 'Module name', 'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence', + 'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.', + 'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.', + 'UI:Button:Analyze' => 'Analyze', 'UI:Button:ModifyChoices' => 'Modify Choices', 'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup', diff --git a/datamodels/2.x/combodo-data-feature-removal/dictionaries/fr.dict.combodo-data-feature-removal.php b/datamodels/2.x/combodo-data-feature-removal/dictionaries/fr.dict.combodo-data-feature-removal.php index ff619ba4d5..c6a65b50f0 100644 --- a/datamodels/2.x/combodo-data-feature-removal/dictionaries/fr.dict.combodo-data-feature-removal.php +++ b/datamodels/2.x/combodo-data-feature-removal/dictionaries/fr.dict.combodo-data-feature-removal.php @@ -21,6 +21,7 @@ Dict::Add('FR FR', 'French', 'Français', [ 'DataFeatureRemoval:Helper:Desc2' => 'Analyse si des données ou des dépendances empêchent l’activation ou la désactivation d’une fonctionnalité.', 'DataFeatureRemoval:Features:Title' => 'Fonctionnalités', + 'DataFeatureRemoval:Execution:Title' => 'Suppressions', 'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse', 'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre', @@ -35,6 +36,9 @@ Dict::Add('FR FR', 'French', 'Français', [ 'DataFeatureRemoval:Table:Analysis:Module' => 'Module', 'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence', + 'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.', + 'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.', + 'UI:Button:Analyze' => 'Analyser', 'UI:Button:ModifyChoices' => 'Modifier les choix', 'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration', diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php b/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php index f7f32be0c8..debd2655f1 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Controller/DataFeatureRemovalController.php @@ -10,13 +10,17 @@ namespace Combodo\iTop\DataFeatureRemoval\Controller; require_once APPROOT.'setup/feature_removal/SetupAudit.php'; require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php'; +use Combodo\iTop\Application\Helper\Session; use Combodo\iTop\Application\TwigBase\Controller\Controller; +use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity; use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException; use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper; +use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog; use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService; use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService; use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment; use Combodo\iTop\Setup\FeatureRemoval\SetupAudit; +use ContextTag; use Dict; use Exception; use IssueLog; @@ -26,9 +30,11 @@ use utils; class DataFeatureRemovalController extends Controller { - private array $aSelectedExtensionsForCheck = []; + private array $aRemovedExtensionsForCheck = []; private array $aCountClassesToCleanup = []; private array $aAnalysisDataTable = []; + private array $aDeletionExecutionSummary = []; + private int $iCount = 0; public function OperationMain($sErrorMessage = null): void @@ -75,7 +81,7 @@ class DataFeatureRemovalController extends Controller $this->m_sOperation = 'Main'; try { - if (count($this->aSelectedExtensionsForCheck) > 0) { + if (count($this->aRemovedExtensionsForCheck) > 0) { $this->Analyze(); } $this->OperationMain(); @@ -87,10 +93,8 @@ class DataFeatureRemovalController extends Controller private function Analyze(): void { + $this->Compile($this->aRemovedExtensionsForCheck); $sSourceEnv = MetaModel::GetEnvironment(); - $oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $this->aSelectedExtensionsForCheck); - $oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnv); - $oSetupAudit = new SetupAudit($sSourceEnv); $aGetRemovedClasses = $oSetupAudit->RunDataAudit(); IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]); @@ -110,56 +114,112 @@ class DataFeatureRemovalController extends Controller } // Display changed extensions - $aAddedExtensions = utils::ReadPostedParam('aAddedExtensions', []); - $aRemovedExtensions = utils::ReadPostedParam('aRemovedExtensions', []); + $aHiddenInputNames = [ + 'selected_modules', + 'selected_extensions', + 'display_choices', + 'added_extensions', + 'removed_extensions', + 'extensions_not_uninstallable', + ]; - IssueLog::Info(__METHOD__.' Extensions given in parameter', null, ['aAddedExtensions' => $aAddedExtensions, 'aRemovedExtensions' => $aRemovedExtensions]); + $aHiddenInputs = []; + foreach ($aHiddenInputNames as $sInputName) { + $aHiddenInputs[$sInputName] = utils::ReadPostedParam($sInputName, "[]", utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + } + $aParams['aHiddenInputs'] = $aHiddenInputs; + + $aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true); + $aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true); + + $aParams['aAddedExtensions'] = $aAddedExtensions; + $aParams['aRemovedExtensions'] = $aRemovedExtensions; + + IssueLog::Debug(__METHOD__.' Extensions given in parameter', null, [ + 'added_extensions' => $aAddedExtensions, + 'removed_extensions' => $aRemovedExtensions]); + + $this->Compile(array_keys($aRemovedExtensions), false); $sSourceEnv = MetaModel::GetEnvironment(); $oSetupAudit = new SetupAudit($sSourceEnv); $aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit()); IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]); - $oDataCleanupService = new DataCleanupService(); - $aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aGetRemovedClasses); - $aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'IssueCount']; - $aRows = []; - $iQueryCount = 0; - $bHasIssues = false; - foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) { - $aRows[] = [ - $oDeletionPlanSummaryEntity->sClass, - $oDeletionPlanSummaryEntity->iDeleteCount, - $oDeletionPlanSummaryEntity->iUpdateCount, - $oDeletionPlanSummaryEntity->iIssueCount, - ]; - $bHasIssues |= ($oDeletionPlanSummaryEntity->iIssueCount !== 0); - $iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount; - $iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount; - } - $aParams['sTransactionId'] = utils::GetNewTransactionId(); - $aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows); $aParams['aClasses'] = $aGetRemovedClasses; - $aParams['iQueryCount'] = $iQueryCount; - $aParams['bDeletionPossible'] = !$bHasIssues; - $aParams['aAddedExtensions'] = $aAddedExtensions; - $aParams['aRemovedExtensions'] = $aRemovedExtensions; $aParams['aExtensions'] = $this->GetExtensionsTableDiff($aAddedExtensions, $aRemovedExtensions); - $this->DisplayPage($aParams); + new ContextTag(ContextTag::TAG_SETUP); + $aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php'; + $aParams['aSetupParams'] = array_merge([ + "_class" => "WizStepLandingBeforeAudit", + "_params[authent]" => SetupUtils::CreateSetupToken(), + "operation" => "next", + ], $aHiddenInputs); + + [$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses); + [$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable(); + $aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0); + Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary)); + + $this->DisplayPage($aParams, 'AnalysisResult'); } - public function OperationDeletionPlan(): void + private function Compile(array $aRemovedExtensions, bool $bForceCompilation = true): void { - $aParams = []; - $this->ValidateTransactionId(); + $sSourceEnv = MetaModel::GetEnvironment(); + $sBuildDir = APPROOT."/env-$sSourceEnv-build"; + if (! is_dir($sBuildDir)) { + SetupUtils::builddir($sBuildDir); + } + $bIsDirEmpty = count(scandir($sBuildDir)) === 2; - $aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS); + if ($bIsDirEmpty || $bForceCompilation) { + $oRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $aRemovedExtensions); + DataFeatureRemovalLog::Debug( + __METHOD__, + null, + ['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")] + ); + $oRuntimeEnvironment->CompileFrom($sSourceEnv); + } + } + private function GetExecutionSummaryTable(): array + { + $sName = 'ExcutionSummary'; + + $aTableData = []; + if (count($this->aDeletionExecutionSummary) === 0) { + return [$aTableData, false]; + } + + $aColumns = ['Class', 'Total Deleted Count' , 'Total Updated Count', 'Deleted Count' , 'Updated Count']; + $aRows = []; + /** @var DataCleanupSummaryEntity $oSummary */ + foreach ($this->aDeletionExecutionSummary as $sClass => $oSummary) { + $aRows[] = [ + $sClass, + $oSummary->iTotalDeleteCount, + $oSummary->iTotalUpdateCount, + $oSummary->iDeleteCount, + $oSummary->iUpdateCount, + ]; + } + + $aTableData = $this->GetTableData($sName, $aColumns, $aRows); + + return [$aTableData, true]; + + } + + private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array + { + $sName = 'DeletionPlanSummary'; $oDataCleanupService = new DataCleanupService(); - $aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aClasses); - $aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'IssueCount']; + $aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses); + $aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count']; $aRows = []; $iQueryCount = 0; $bHasIssues = false; @@ -174,39 +234,31 @@ class DataFeatureRemovalController extends Controller $iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount; $iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount; } - - $aParams['sTransactionId'] = utils::GetNewTransactionId(); - $aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows); - $aParams['aClasses'] = $aClasses; - $aParams['iQueryCount'] = $iQueryCount; - $aParams['bDeletionPossible'] = !$bHasIssues; - - $this->DisplayPage($aParams); + return [$this->GetTableData($sName, $aColumns, $aRows), $iQueryCount, !$bHasIssues]; } public function OperationDoDeletion(): void { - $aParams = []; $this->ValidateTransactionId(); + $this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary')); + Session::Unset('aDeletionExecutionSummary'); $aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS); $oDataCleanupService = new DataCleanupService(); $aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses); - $aColumns = ['Class', 'DeletedCount' , 'UpdatedCount']; - $aRows = []; - foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) { - $aRows[] = [ - $oDeletionExecutionSummaryEntity->sClass, - $oDeletionExecutionSummaryEntity->iDeleteCount, - $oDeletionExecutionSummaryEntity->iUpdateCount, - ]; + foreach ($aDeletionExecutionSummary as $sClass => $oExecutionSummary) { + if (!array_key_exists($sClass, $this->aDeletionExecutionSummary)) { + $this->aDeletionExecutionSummary[$sClass] = new DataCleanupSummaryEntity($sClass); + } + $oSummary = $this->aDeletionExecutionSummary[$sClass]; + $oSummary->iDeleteCount = $oExecutionSummary->iDeleteCount; + $oSummary->iUpdateCount = $oExecutionSummary->iUpdateCount; + $oSummary->iTotalDeleteCount += $oExecutionSummary->iDeleteCount; + $oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount; } - $aParams['sTransactionId'] = utils::GetNewTransactionId(); - $aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows); - - $this->DisplayPage($aParams); + $this->OperationAnalysisResult(); } private function GetExtensionsTableDiff(array $aAddedExtensions, array $aRemovedExtensions): array @@ -256,7 +308,7 @@ HTML, if ($oExtension->bRemovedFromDisk) { $sDisabledHtml = 'disabled=""'; $sChecked = 'checked'; - } elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) { + } elseif (in_array($sCode, $this->aRemovedExtensionsForCheck)) { $sChecked = 'checked'; } @@ -322,7 +374,7 @@ HTML, */ public function ReadRemovedExtensions(): void { - if (count($this->aSelectedExtensionsForCheck) > 0) { + if (count($this->aRemovedExtensionsForCheck) > 0) { return; } @@ -330,14 +382,14 @@ HTML, foreach ($aSelectedExtensionsFromUI as $sCode => $aData) { $sValue = $aData['enable'] ?? 'off'; if (($sValue) === 'on') { - $this->aSelectedExtensionsForCheck[] = $sCode; + $this->aRemovedExtensionsForCheck[] = $sCode; } } // Add source removed to check foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) { if ($oExtension->bRemovedFromDisk) { - $this->aSelectedExtensionsForCheck[] = $sCode; + $this->aRemovedExtensionsForCheck[] = $sCode; } } } diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php index 2bfe87f783..c227fdeea9 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php @@ -8,6 +8,8 @@ class DataCleanupSummaryEntity public int $iIssueCount = 0; public int $iUpdateCount = 0; public int $iDeleteCount = 0; + public int $iTotalUpdateCount = 0; + public int $iTotalDeleteCount = 0; /** * @param string $sClass diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php index 73c428f296..6e209b78c1 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php @@ -23,27 +23,29 @@ class ObjectServiceSummary implements iObjectService public function Update(DBObject $oToUpdate, string $sAttCode, $value): void { $sClass = get_class($oToUpdate); - DataFeatureRemovalLog::Info('Object to update', null, ['class' => $sClass, 'id' => $oToUpdate->GetKey(), 'code' => $sAttCode, 'value' => "$value"]); + DataFeatureRemovalLog::Debug('Object to update', null, ['class' => $sClass, 'id' => $oToUpdate->GetKey(), 'code' => $sAttCode, 'value' => "$value"]); if (! array_key_exists($sClass, $this->aSummary)) { $this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass); } $oDeletionPlanSummaryEntity = $this->aSummary[$sClass]; $oDeletionPlanSummaryEntity->iUpdateCount++; + $oDeletionPlanSummaryEntity->iTotalUpdateCount++; } public function Delete(string $sClass, string $sId): void { - DataFeatureRemovalLog::Info('Object to delete', null, ['class' => $sClass, 'id' => $sId]); + DataFeatureRemovalLog::Debug('Object to delete', null, ['class' => $sClass, 'id' => $sId]); if (!array_key_exists($sClass, $this->aSummary)) { $this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass); } $oDeletionPlanSummaryEntity = $this->aSummary[$sClass]; $oDeletionPlanSummaryEntity->iDeleteCount++; + $oDeletionPlanSummaryEntity->iTotalDeleteCount++; } public function SetIssue(string $sClass): void { - DataFeatureRemovalLog::Info('Issue on object', null, ['class' => $sClass]); + DataFeatureRemovalLog::Debug('Issue on object', null, ['class' => $sClass]); if (!array_key_exists($sClass, $this->aSummary)) { $this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass); } @@ -55,4 +57,15 @@ class ObjectServiceSummary implements iObjectService { return $this->aSummary; } + + public function SetSummary(array $aSummary): void + { + foreach ($aSummary as $sClass => $oPreviousSummaryEntity) { + $oSummaryEntity = new DataCleanupSummaryEntity($sClass); + $oSummaryEntity->iTotalUpdateCount = $oPreviousSummaryEntity->iTotalUpdateCount; + $oSummaryEntity->iTotalDeleteCount = $oPreviousSummaryEntity->iTotalDeleteCount; + + $this->aSummary[$sClass] = $oSummaryEntity; + } + } } diff --git a/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig b/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig index 053a805ed5..e585003983 100644 --- a/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig +++ b/datamodels/2.x/combodo-data-feature-removal/templates/AnalysisResult.html.twig @@ -9,23 +9,48 @@ {% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %} {% EndUIFieldSet %} - {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %} - {% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %} - {% EndUIFieldSet %} - - {% if bDeletionPossible %} - {% UIForm Standard {} %} - {% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %} - {% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %} - {% for sKey, sClass in aClasses %} - {% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %} - {% endfor %} - {% UIToolbar ForButton {} %} - {% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %} - {% EndUIToolbar %} - {% EndUIForm %} + {% if bDeletionNeeded %} + {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %} + {% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %} + {% EndUIFieldSet %} + {% if bDeletionPossible %} + {% UIForm Standard {} %} + {% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %} + {% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %} + {% for sKey, sClass in aClasses %} + {% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %} + {% endfor %} + {% for sCode, sLabel in aAddedExtensions %} + {% UIInput ForHidden { sName:"aAddedExtensions[" ~ sCode ~ "]", sValue:sLabel } %} + {% endfor %} + {% for sCode, sLabel in aRemovedExtensions %} + {% UIInput ForHidden { sName:"aRemovedExtensions[" ~ sCode ~ "]", sValue:sLabel } %} + {% endfor %} + {% for sInputName, sValue in aHiddenInputs %} + {% UIInput ForHidden { sName:sInputName, sValue:sValue } %} + {% endfor %} + {% UIToolbar ForButton {} %} + {% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %} + {% EndUIToolbar %} + {% EndUIForm %} + {% else %} + {% UIAlert ForFailure { sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %} + {% endif %} {% else %} - {{ 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s }} + {% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s, sId:value } %}{% EndUIAlert %} + + {% UIForm Standard {'sId':'launch-setup-form', Action:sLaunchSetupUrl, 'EncType': 'application/x-www-form-urlencoded'} %} + {% for sKey, sValue in aSetupParams %} + {% UIInput ForHidden { sName:sKey, sValue:sValue } %} + {% endfor %} + {% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %} + {% EndUIForm %} + {% endif %} + + {% if bHasDeletionExecution %} + {% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %} + {% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %} + {% EndUIFieldSet %} {% endif %} {% UIForm Standard {} %} diff --git a/setup/extensionsmap.class.inc.php b/setup/extensionsmap.class.inc.php index 8c240f9fc9..11dc6e063a 100644 --- a/setup/extensionsmap.class.inc.php +++ b/setup/extensionsmap.class.inc.php @@ -43,48 +43,53 @@ class iTopExtensionsMap * * @param string $sFromEnvironment The environment to scan * @param array $aExtraDirs extensions dir to scan + * @param array $aExtraDirs extensions dir to scan + * @param string|null $sAppRootForTests * * @return void */ - public function __construct(string $sFromEnvironment = ITOP_DEFAULT_ENV, array $aExtraDirs = []) + public function __construct(string $sFromEnvironment = ITOP_DEFAULT_ENV, array $aExtraDirs = [], ?string $sAppRootForTests = null) { $this->aExtensions = []; $this->aExtensionsByCode = []; $this->aScannedDirs = []; - $this->ScanDisk($sFromEnvironment); + + $sAppRoot = $sAppRootForTests ?? APPROOT; + $this->ScanDisk($sFromEnvironment, $sAppRoot); $this->aExtraDirs = $aExtraDirs; - if (is_dir(APPROOT.'extensions')) { - $this->aExtraDirs [] = APPROOT.'extensions'; + if (is_dir($sAppRoot.'extensions')) { + $this->aExtraDirs [] = $sAppRoot.'extensions'; } - if (is_dir(APPROOT.'data/'.$sFromEnvironment.'-modules')) { - $this->aExtraDirs [] = APPROOT.'data/'.$sFromEnvironment.'-modules'; + if (is_dir($sAppRoot.'data/'.$sFromEnvironment.'-modules')) { + $this->aExtraDirs [] = $sAppRoot.'data/'.$sFromEnvironment.'-modules'; } foreach ($aExtraDirs as $sDir) { $this->ReadDir($sDir, iTopExtension::SOURCE_REMOTE); } - $this->CheckDependencies(); + $this->CheckDependencies($sAppRoot); } /** * Populate the list of available (pseudo)extensions by scanning the disk * where the iTop files are located * @param string $sEnvironment + * @param string $sAppRoot * @return void */ - protected function ScanDisk($sEnvironment) + protected function ScanDisk($sEnvironment, string $sAppRoot) { - if (!$this->ReadInstallationWizard(APPROOT.'/datamodels/2.x')) { + if (!$this->ReadInstallationWizard($sAppRoot.'/datamodels/2.x')) { $this->bHasXmlInstallationFile = false; //no installation xml found in 2.x: let's read all extensions in 2.x first - if (!$this->ReadDir(APPROOT.'datamodels/2.x', iTopExtension::SOURCE_WIZARD)) { + if (!$this->ReadDir($sAppRoot.'datamodels/2.x', iTopExtension::SOURCE_WIZARD)) { //nothing found in 2.x : fallback read in 1.x (flat structure) - $this->ReadDir(APPROOT.'datamodels/1.x', iTopExtension::SOURCE_WIZARD); + $this->ReadDir($sAppRoot.'datamodels/1.x', iTopExtension::SOURCE_WIZARD); } } - $this->ReadDir(APPROOT.'extensions', iTopExtension::SOURCE_MANUAL); - $this->ReadDir(APPROOT.'data/'.$sEnvironment.'-modules', iTopExtension::SOURCE_REMOTE); + $this->ReadDir($sAppRoot.'extensions', iTopExtension::SOURCE_MANUAL); + $this->ReadDir($sAppRoot.'data/'.$sEnvironment.'-modules', iTopExtension::SOURCE_REMOTE); } /** @@ -99,24 +104,58 @@ class iTopExtensionsMap return false; } + $aModuleConfigs = []; + $this->ListModuleFiles(basename($sDir), dirname($sDir), $aModuleConfigs); + $oXml = new XMLParameters($sDir.'/installation.xml'); foreach ($oXml->Get('steps') as $aStepInfo) { if (array_key_exists('options', $aStepInfo)) { - $this->ProcessWizardChoices($aStepInfo['options']); + $this->ProcessWizardChoices($aStepInfo['options'], $aModuleConfigs); } if (array_key_exists('alternatives', $aStepInfo)) { - $this->ProcessWizardChoices($aStepInfo['alternatives']); + $this->ProcessWizardChoices($aStepInfo['alternatives'], $aModuleConfigs); } } + return true; } + private function ListModuleFiles(string $sRelDir, string $sRootDir, array &$aRes): void + { + $sDirectory = $sRootDir.'/'.$sRelDir; + + if ($hDir = opendir($sDirectory)) { + // This is the correct way to loop over the directory. (according to the documentation) + while (($sFile = readdir($hDir)) !== false) { + $aMatches = []; + if (is_dir($sDirectory.'/'.$sFile)) { + if (($sFile != '.') && ($sFile != '..') && ($sFile != '.svn') && ($sFile != 'vendor')) { + $this->ListModuleFiles($sRelDir.'/'.$sFile, $sRootDir, $aRes); + } + } elseif (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) { + try { + $aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sDirectory.'/'.$sFile); + $sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID]; + list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId); + $aModuleConfig = $aModuleInfo[ModuleFileReader::MODULE_INFO_CONFIG]; + $aModuleConfig['module_version'] = $sModuleVersion; + $aRes[$sModuleName] = $aModuleConfig; + } catch (ModuleFileReaderException $e) { + continue; + } + } + } + closedir($hDir); + } + } + /** * Helper to process a "choice" array read from the installation.xml file * @param array $aChoices + * @param array $aModuleConfigs * @return void */ - protected function ProcessWizardChoices($aChoices) + protected function ProcessWizardChoices($aChoices, $aModuleConfigs) { foreach ($aChoices as $aChoiceInfo) { if (array_key_exists('extension_code', $aChoiceInfo)) { @@ -128,13 +167,23 @@ class iTopExtensionsMap if (array_key_exists('modules', $aChoiceInfo)) { // Some wizard choices are not associated with any module $oExtension->aModules = $aChoiceInfo['modules']; + foreach ($oExtension->aModules as $sModuleName) { + $aCurrentModuleConfig = $aModuleConfigs[$sModuleName] ?? null; + if (is_null($aCurrentModuleConfig)) { + IssueLog::Debug("Installation choice comes with missing module file", null, ["choice" => $oExtension->sCode, 'module' => $sModuleName]); + continue; + } + $oExtension->aModuleVersion[$sModuleName] = $aCurrentModuleConfig['module_version']; + unset($aCurrentModuleConfig['module_version']); + $oExtension->aModuleInfo[$sModuleName] = $aCurrentModuleConfig; + } } if (array_key_exists('sub_options', $aChoiceInfo)) { if (array_key_exists('options', $aChoiceInfo['sub_options'])) { - $this->ProcessWizardChoices($aChoiceInfo['sub_options']['options']); + $this->ProcessWizardChoices($aChoiceInfo['sub_options']['options'], $aModuleConfigs); } if (array_key_exists('alternatives', $aChoiceInfo['sub_options'])) { - $this->ProcessWizardChoices($aChoiceInfo['sub_options']['alternatives']); + $this->ProcessWizardChoices($aChoiceInfo['sub_options']['alternatives'], $aModuleConfigs); } } $this->AddExtension($oExtension); @@ -207,7 +256,7 @@ class iTopExtensionsMap $oExtension = $this->GetFromExtensionCode($sCode); if (!is_null($oExtension)) { $aRemovedExtension [] = $oExtension; - \IssueLog::Info(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]); + \IssueLog::Debug(__METHOD__.": remove extension locally", null, ['extension_code' => $oExtension->sCode]); } else { \IssueLog::Warning(__METHOD__." cannot find extensions", null, ['code' => $sCode]); } @@ -334,19 +383,19 @@ class iTopExtensionsMap /** * Check if some extension contains a module with missing dependencies... * If so, populate the aMissingDepenencies array + * @param string $sAppRoot * @return void */ - protected function CheckDependencies() + protected function CheckDependencies(string $sAppRoot) { $aSearchDirs = []; - if (is_dir(APPROOT.'/datamodels/2.x')) { - $aSearchDirs[] = APPROOT.'/datamodels/2.x'; - } elseif (is_dir(APPROOT.'/datamodels/1.x')) { - $aSearchDirs[] = APPROOT.'/datamodels/1.x'; + if (is_dir($sAppRoot.'/datamodels/2.x')) { + $aSearchDirs[] = $sAppRoot.'/datamodels/2.x'; + } elseif (is_dir($sAppRoot.'/datamodels/1.x')) { + $aSearchDirs[] = $sAppRoot.'/datamodels/1.x'; } $aSearchDirs = array_merge($aSearchDirs, $this->aScannedDirs); - try { ModuleDiscovery::GetModulesOrderedByDependencies($aSearchDirs, true); } catch (MissingDependencyException $e) { diff --git a/setup/feature_removal/AbstractSetupAudit.php b/setup/feature_removal/AbstractSetupAudit.php index 9a85b56cbd..b7696546e0 100644 --- a/setup/feature_removal/AbstractSetupAudit.php +++ b/setup/feature_removal/AbstractSetupAudit.php @@ -107,7 +107,7 @@ abstract class AbstractSetupAudit if (ContextTag::Check(ContextTag::TAG_SETUP)) { SetupLog::Info($sMessage, $sChannel, $aContext); } else { - IssueLog::Info($sMessage, $sChannel, $aContext); + IssueLog::Debug($sMessage, $sChannel, $aContext); } } } diff --git a/setup/feature_removal/ModelReflectionSerializer.php b/setup/feature_removal/ModelReflectionSerializer.php index 53ed1d5dfa..321b79160e 100644 --- a/setup/feature_removal/ModelReflectionSerializer.php +++ b/setup/feature_removal/ModelReflectionSerializer.php @@ -6,7 +6,6 @@ use ContextTag; use CoreException; use Exception; use IssueLog; -use MetaModel; use SetupLog; use utils; @@ -34,7 +33,7 @@ class ModelReflectionSerializer public function GetModelFromEnvironment(string $sEnv): array { - IssueLog::Info(__METHOD__, null, ['env' => $sEnv]); + IssueLog::Debug(__METHOD__, null, ['env' => $sEnv]); $sPHPExec = trim(utils::GetConfig()->Get('php_path')); $sOutput = ""; diff --git a/setup/itopextension.class.inc.php b/setup/itopextension.class.inc.php index 81344f9e7c..b40a64d74a 100644 --- a/setup/itopextension.class.inc.php +++ b/setup/itopextension.class.inc.php @@ -141,4 +141,32 @@ class iTopExtension } return true; } + + public function __serialize(): array + { + return [ + 'sCode' => $this->sCode, + 'sSource' => $this->sSource, + 'sVersion' => $this->sVersion, + 'aModules' => $this->aModules, + 'aModuleVersion' => $this->aModuleVersion, + 'aModuleInfo' => $this->aModuleInfo, + ]; + } + + public function __unserialize(array $aData): void + { + $this->sCode = $aData['sCode'] ?? ''; + $this->sSource = $aData['sSource'] ?? ''; + $this->sVersion = $aData['sVersion'] ?? ''; + $this->aModules = $aData['aModules'] ?? ''; + $this->aModuleVersion = $aData['aModuleVersion'] ?? ''; + $this->aModuleInfo = $aData['aModuleInfo'] ?? ''; + } + + public function __toString(): string + { + return json_encode($this->__serialize(), JSON_PRETTY_PRINT); + } + } diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php index 7ac863c23a..48422d9cdb 100755 --- a/setup/modulediscovery.class.inc.php +++ b/setup/modulediscovery.class.inc.php @@ -355,9 +355,11 @@ class ModuleDiscovery */ protected static function ListModuleFiles($sRelDir, $sRootDir) { - static $iDummyClassIndex = 0; $sDirectory = $sRootDir.'/'.$sRelDir; + if (!is_dir(utils::RealPath($sDirectory, APPROOT))) { + throw new Exception('Data directory ('.$sDirectory.') Does not exist or is outside iTop.'); + } if ($hDir = opendir($sDirectory)) { // This is the correct way to loop over the directory. (according to the documentation) while (($sFile = readdir($hDir)) !== false) { @@ -425,14 +427,15 @@ class ModuleDiscovery continue; } - SetupLog::Info("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]); + IssueLog::Debug("Module considered as removed", null, ['extension_code' => $oExtension->sCode, 'module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sCurrentModuleFilePath]); return true; } if (count($aNonMatchingPaths) > 0) { //add log for support - SetupLog::Debug("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]); + IssueLog::Debug("Module kept as it came from non removed extensions", null, ['module_name' => $sModuleName, 'module_version' => $sModuleVersion, ModuleFileReader::MODULE_FILE_PATH => $sModuleFilePath, 'non_matching_paths' => $aNonMatchingPaths]); } + IssueLog::Debug(__METHOD__.' Module loaded', null, ['name' => $sModuleName, 'version' => $sModuleVersion]); return false; } diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 5e8362bbc5..25974efb9a 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -99,6 +99,7 @@ class RunTimeEnvironment $this->sBuildEnv = $sEnvironment.'-build'; } $this->oExtensionsMap = null; + SetupLog::Enable(APPROOT.'log/setup.log'); } /** diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index d04ceb525f..f48f0a53cc 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -1576,11 +1576,13 @@ JS * @return array * @throws Exception */ - public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null) + public static function AnalyzeInstallation($oWizard, $bAbortOnMissingDependency = false, $aModulesToLoad = null, Config $oConfig = null) { require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); - $oConfig = self::GetConfig($oWizard); + if (is_null($oConfig)) { + $oConfig = self::GetConfig($oWizard); + } $aDirsToScan = [$oWizard->GetParameter('source_dir', '')]; diff --git a/setup/wizardcontroller.class.inc.php b/setup/wizardcontroller.class.inc.php index 3c914dd1a5..eace6760da 100644 --- a/setup/wizardcontroller.class.inc.php +++ b/setup/wizardcontroller.class.inc.php @@ -38,7 +38,7 @@ require_once(APPROOT.'setup/extensionsmap.class.inc.php'); class WizardController { - protected $aSteps; + protected $aWizardSteps; protected $sInitialStepClass; protected $sInitialState; protected $aParameters; @@ -53,7 +53,7 @@ class WizardController $this->sInitialStepClass = $sInitialStepClass; $this->sInitialState = $sInitialState; $this->aParameters = []; - $this->aSteps = []; + $this->aWizardSteps = []; } /** @@ -62,7 +62,7 @@ class WizardController */ protected function PushStep($aStepInfo) { - array_push($this->aSteps, $aStepInfo); + array_push($this->aWizardSteps, $aStepInfo); } /** @@ -71,7 +71,7 @@ class WizardController */ protected function PopStep() { - return array_pop($this->aSteps); + return array_pop($this->aWizardSteps); } /** @@ -235,9 +235,9 @@ HTML; $oPage->add(''); } - $oPage->add(''); + $oPage->add(''); $oPage->add('
| '); } if ($oStep->CanMoveForward()) { @@ -296,7 +296,7 @@ on the page's parameters $sOperation = utils::ReadParam('operation'); $this->aParameters = utils::ReadParam('_params', [], false, 'raw_data'); - $this->aSteps = json_decode(utils::ReadParam('_steps', '[]', false, 'raw_data'), true /* bAssoc */); + $this->SetWizardSteps(json_decode(utils::ReadParam('_steps', '[]', false, 'raw_data'), true)); switch ($sOperation) { case 'next': @@ -371,6 +371,11 @@ on the page's parameters return $sOutput; } + public function SetWizardSteps(array $aWizardSteps): void + { + $this->aWizardSteps = $aWizardSteps; + } + /** * @param string $sCurrentStepClass * @param string $sCurrentState diff --git a/setup/wizardsteps/WizStepDataAudit.php b/setup/wizardsteps/WizStepDataAudit.php index 88c25080db..d6288d87a1 100644 --- a/setup/wizardsteps/WizStepDataAudit.php +++ b/setup/wizardsteps/WizStepDataAudit.php @@ -100,39 +100,30 @@ JS); { $sApplicationUrl = utils::GetAbsoluteUrlModulePage('combodo-data-feature-removal', 'index.php'); - $aRemovedExtensions = json_decode($this->oWizard->GetParameter('removed_extensions', '[]'), true); - $aHiddenRemovedExtensionInputs = ''; - if (!is_array($aRemovedExtensions)) { - IssueLog::Warning('Posted removed_extensions is not an array'); - $aRemovedExtensions = []; - } - foreach ($aRemovedExtensions as $sExtCode => $sExtLabel) { - $sSafeExtCode = utils::HtmlEntities($sExtCode); - $aHiddenRemovedExtensionInputs .= << + $aParams = [ + 'selected_modules', + 'selected_extensions', + 'display_choices', + 'added_extensions', + 'removed_extensions', + 'extensions_not_uninstallable', + ]; + $aHiddenInputs = ''; + foreach ($aParams as $sParamName) { + $sElements = utils::HtmlEntities($this->oWizard->GetParameter($sParamName, '[]')); + $sParamName = utils::HtmlEntities($sParamName); + $aHiddenInputs .= << INPUT; } - $aAddedExtensions = json_decode($this->oWizard->GetParameter('extensions_added', "[]"), true); - $aHiddenAddedExtensionInputs = ""; - if (!is_array($aAddedExtensions)) { - IssueLog::Warning('Posted extensions_added is not an array'); - $aAddedExtensions = []; - } - foreach ($aAddedExtensions as $sExtCode => $sExtLabel) { - $sSafeExtCode = utils::HtmlEntities($sExtCode); - $aHiddenAddedExtensionInputs .= << -INPUT; - } $sUID = Session::Get('setup_token'); $oPage->add( << - $aHiddenRemovedExtensionInputs - $aHiddenAddedExtensionInputs + $aHiddenInputs HTML ); @@ -141,7 +132,6 @@ HTML protected function AddProgressErrorScript($oPage, $aRes) { if (isset($aRes['error_code']) && $aRes['error_code'] === DataAuditSequencer::DATA_AUDIT_FAILED) { - $oPage->add_ready_script( << |