From ba6cc9dcbab20342b641eba20b413f5c90c18127 Mon Sep 17 00:00:00 2001 From: odain-cbd <56586767+odain-cbd@users.noreply.github.com> Date: Wed, 20 May 2026 10:16:32 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B09567=20-=20Extension=20Mgmt=20:=20Run=20?= =?UTF-8?q?setup=20(#912)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * N°9144 - correct next button in audit page * N°9567 - WIP code style * N°9412 - Screen Analysis results wip + test endpoint * N°9412 - next button label * N°9412 - Mask CI pbs * N°9412 - Analysis results screen wip * N°9412 - Analysis results screen wip * N°9567 - fix extension map init of installation choices * N°9567 - fix test * N°9567 - link from ext mgt to setup WIP * N°9567 - add enc_type in UIForm to be able to change content type in twigs * N°9412 - wip * N°9567 - Extension Mgmt : Run setup * N°9567 - Extension Mgmt : Run setup * N°9567 - Extension Mgmt : Run setup (fix post-deletion button inputs) * N°9567 - Extension Mgmt : Run setup * N°9567 - Extension Mgmt : Run setup * N°9567 - Extension Mgmt : Run setup --------- Co-authored-by: Eric Espie --- .../en.dict.combodo-data-feature-removal.php | 4 + .../fr.dict.combodo-data-feature-removal.php | 4 + .../DataFeatureRemovalController.php | 178 ++++--- .../src/Entity/DataCleanupSummaryEntity.php | 2 + .../src/Service/ObjectServiceSummary.php | 19 +- .../templates/AnalysisResult.html.twig | 57 ++- setup/extensionsmap.class.inc.php | 99 +++- setup/feature_removal/AbstractSetupAudit.php | 2 +- .../ModelReflectionSerializer.php | 3 +- setup/itopextension.class.inc.php | 28 ++ setup/modulediscovery.class.inc.php | 9 +- setup/runtimeenv.class.inc.php | 1 + setup/setuputils.class.inc.php | 6 +- setup/wizardcontroller.class.inc.php | 19 +- setup/wizardsteps/WizStepDataAudit.php | 40 +- .../wizardsteps/WizStepLandingBeforeAudit.php | 95 ++++ setup/wizardsteps/WizStepModulesChoice.php | 119 ++++- setup/wizardsteps/WizStepSummary.php | 3 +- setup/wizardsteps_autoload.php | 1 + .../UI/Base/Component/Form/Form.php | 23 + .../base/components/form/layout.html.twig | 2 +- .../DataCleanupServiceTest.php | 64 ++- .../simulate-audit-from-setup.php | 125 +++++ .../unitary-tests/setup/ExtensionsMapTest.php | 31 ++ .../setup/WizStepModulesChoiceTest.php | 12 + .../all_extensions_from_datamodels.json | 316 ++++++++++++ .../datamodels/2.x/installation.xml | 243 ++++++++++ .../module.itop-config-mgmt.php | 107 +++++ .../module.itop-service-mgmt.php | 89 ++++ .../itop-structure/module.itop-structure.php | 449 ++++++++++++++++++ .../setup/ressources/installation_330.xml | 243 ++++++++++ 31 files changed, 2221 insertions(+), 172 deletions(-) create mode 100644 setup/wizardsteps/WizStepLandingBeforeAudit.php create mode 100644 tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/all_extensions_from_datamodels.json create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/installation.xml create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-structure/module.itop-structure.php create mode 100644 tests/php-unit-tests/unitary-tests/setup/ressources/installation_330.xml 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 ((count($this->aSteps) > 0) && ($oStep->CanMoveBackward())) { + if ((count($this->aWizardSteps) > 0) && ($oStep->CanMoveBackward())) { $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( <<'); diff --git a/setup/wizardsteps/WizStepLandingBeforeAudit.php b/setup/wizardsteps/WizStepLandingBeforeAudit.php new file mode 100644 index 0000000000..a848f372cc --- /dev/null +++ b/setup/wizardsteps/WizStepLandingBeforeAudit.php @@ -0,0 +1,95 @@ +GetBuildEnv().'/'.ITOP_CONFIG_FILE; + $this->oConfig = new Config($sBuildConfigFile); + + $oWizard->SetParameter('previous_version_dir', APPROOT); + $oWizard->SetParameter('install_mode', 'upgrade'); + $oWizard->SetParameter('source_dir', APPROOT.$this->oConfig->Get('source_dir')); + $oWizard->SetParameter('graphviz_path', $this->oConfig->Get('graphviz_path')); + $oWizard->SetParameter('application_url', $this->oConfig->Get('app_root_url')); + $oWizard->SetParameter('datamodel_version', ITOP_CORE_VERSION); + $oWizard->SetParameter('upgrade_type', 'use-compatible'); + + $oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks()); + $oWizard->SaveParameter('force-uninstall', ''); + + // should be done at the end + parent::__construct($oWizard, $sCurrentState, false); + } + + /** + * @inheritDoc + */ + public function Display(SetupPage $oPage): void + { + } + + /** + * @inheritDoc + */ + public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState + { + $oProductionEnv = new RunTimeEnvironment(); + $sBuildConfigFile = APPCONF.$oProductionEnv->GetBuildEnv().'/'.ITOP_CONFIG_FILE; + @chmod($sBuildConfigFile, 0770); // In case it exists: RWX for owner and group, nothing for others + + $oConfig = new Config($sBuildConfigFile); + $this->oWizard->SetParameter('db_server', $oConfig->Get('db_host')); + $this->oWizard->SetParameter('db_user', $oConfig->Get('db_user')); + $this->oWizard->SetParameter('db_pwd', $oConfig->Get('db_pwd')); + $this->oWizard->SetParameter('db_name', $oConfig->Get('db_name')); + $this->oWizard->SetParameter('db_prefix', $oConfig->Get('db_subname')); + $this->oWizard->SetParameter('db_tls_enabled', $oConfig->Get('db_tls.enabled')); + $this->oWizard->SetParameter('db_tls_ca', $oConfig->Get('db_tls.ca') ?? ''); + + $this->oWizard->SaveParameter('selected_modules', []); + $this->oWizard->SaveParameter('selected_extensions', []); + $this->oWizard->SaveParameter('display_choices', []); + $this->oWizard->SaveParameter('added_extensions', []); + $this->oWizard->SaveParameter('removed_extensions', []); + $this->oWizard->SaveParameter('extensions_not_uninstallable', []); + + $aWizardSteps = $this->GetWizardSteps(); + $this->oWizard->SetWizardSteps($aWizardSteps); + $this->sCurrentState = count($aWizardSteps) - 1; + + $aSelectedComponents = $this->GetSelectedComponents($this->aSteps, $this->oWizard->GetParameter('selected_extensions')); + $this->oWizard->SetParameter('selected_components', json_encode($aSelectedComponents)); + + return new WizardState(WizStepDataAudit::class); + } + + /** + * @inheritDoc + */ + public function GetTitle(): string + { + return 'Before checking compatibility'; + } + + public function GetPossibleSteps() + { + return [WizStepDataAudit::class]; + } + + public function GetNextButtonLabel() + { + return 'Next'; + } + + public function CanComeBack() + { + return false; + } +} diff --git a/setup/wizardsteps/WizStepModulesChoice.php b/setup/wizardsteps/WizStepModulesChoice.php index 8474588d69..a414599ba6 100644 --- a/setup/wizardsteps/WizStepModulesChoice.php +++ b/setup/wizardsteps/WizStepModulesChoice.php @@ -25,7 +25,7 @@ use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; /** * Choice of the modules to be installed */ -class WizStepModulesChoice extends WizardStep +class WizStepModulesChoice extends AbstractWizStepInstall { protected static string $SEP = '_'; protected bool $bUpgrade = false; @@ -38,7 +38,7 @@ class WizStepModulesChoice extends WizardStep */ protected iTopExtensionsMap $oExtensionsMap; - private ?array $aSteps = null; + protected ?array $aSteps = null; protected PhpExpressionEvaluator $oPhpExpressionEvaluator; @@ -51,7 +51,7 @@ class WizStepModulesChoice extends WizardStep private array $aAnalyzeInstallationModules = []; private ?MissingDependencyException $oMissingDependencyException = null; - public function __construct(WizardController $oWizard, $sCurrentState) + public function __construct(WizardController $oWizard, $sCurrentState, bool $bOverWriteConfig = true) { parent::__construct($oWizard, $sCurrentState); $this->bChoicesFromDatabase = false; @@ -69,8 +69,10 @@ class WizStepModulesChoice extends WizardStep if ($sConfigPath !== null) { $this->oConfig = new Config($sConfigPath); - $aParamValues = $oWizard->GetParamForConfigArray(); - $this->oConfig->UpdateFromParams($aParamValues); + if ($bOverWriteConfig) { + $aParamValues = $oWizard->GetParamForConfigArray(); + $this->oConfig->UpdateFromParams($aParamValues); + } $this->oExtensionsMap->LoadChoicesFromDatabase($this->oConfig); $this->bChoicesFromDatabase = true; @@ -78,7 +80,7 @@ class WizStepModulesChoice extends WizardStep // Sanity check (not stopper, to let developers go further...) try { - $this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard, true); + $this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard, true, null, $this->oConfig); } catch (MissingDependencyException $e) { $this->oMissingDependencyException = $e; $this->aAnalyzeInstallationModules = SetupUtils::AnalyzeInstallation($this->oWizard); @@ -118,17 +120,6 @@ class WizStepModulesChoice extends WizardStep return [$aExtensionsAdded, $aExtensionsRemoved, $aExtensionsNotUninstallable]; } - public function IsDataAuditEnabled(): bool - { - $sPath = APPROOT.'env-production'; - if (!is_dir($sPath)) { - SetupLog::Info("Reinstallation of an iTop from a backup (No env-production found). Setup data audit disabled"); - - return false; - } - return true; - } - public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState { // Accumulates the selected modules: @@ -163,7 +154,7 @@ class WizStepModulesChoice extends WizardStep $this->oWizard->SetParameter('selected_modules', json_encode(array_keys($aModules))); $this->oWizard->SetParameter('selected_extensions', json_encode($aExtensions)); $this->oWizard->SetParameter('display_choices', $sDisplayChoices); - $this->oWizard->SetParameter('extensions_added', json_encode($aExtensionsAdded)); + $this->oWizard->SetParameter('added_extensions', json_encode($aExtensionsAdded)); $this->oWizard->SetParameter('removed_extensions', json_encode($aExtensionsRemoved)); $this->oWizard->SetParameter('extensions_not_uninstallable', json_encode(array_keys($aExtensionsNotUninstallable))); @@ -175,6 +166,96 @@ class WizStepModulesChoice extends WizardStep return new WizardState(WizStepModulesChoice::class, (string)($index - 1)); } + public function GetWizardSteps(): array + { + $aSteps = [ + ["class" => "WizStepWelcome","state" => ""], + ["class" => "WizStepInstallOrUpgrade","state" => ""], + ["class" => "WizStepDetectedInfo","state" => ""], + ["class" => "WizStepUpgradeMiscParams","state" => ""], + ]; + $i = 0; + while (null != $this->GetStepInfo($i)) { + $aSteps [] = ["class" => "WizStepModulesChoice","state" => "$i"]; + $i++; + } + + return $aSteps; + } + + public function GetSelectedComponents(array $aSteps, string $sSelectedExtensionJson): array + { + SetupLog::Error(__METHOD__, null, $aSteps); + $aExtensions = json_decode($sSelectedExtensionJson, true); + $aRes = []; + foreach ($aSteps as $i => $aStepInfo) { + $aStepRes = []; + $this->ProcessOptions("", $aStepInfo, $aExtensions, $aStepRes); + $this->ProcessAlternatives("", $aStepInfo, $aExtensions, $aStepRes); + $aRes [] = $aStepRes; + } + + return $aRes; + } + + public function ProcessOptions(string $sCurrentIndex, array $aInfo, array $aExtensions, array &$aStepRes) + { + $aOptions = $aInfo["options"] ?? null; + if (is_null($aOptions) || !is_array($aOptions)) { + return; + } + + foreach ($aOptions as $i => $aOptionsInfo) { + $sExtensionCode = $aOptionsInfo["extension_code"] ?? null; + + if (in_array($sExtensionCode, $aExtensions)) { + $aStepRes = $this->ProcessSelectedOption($sCurrentIndex, $i, $aStepRes, $aOptionsInfo, $aExtensions); + } + } + } + + public function ProcessAlternatives(string $sCurrentIndex, array $aInfo, array $aExtensions, array &$aStepRes) + { + $aAlternatives = $aInfo["alternatives"] ?? null; + if (is_null($aAlternatives) || ! is_array($aAlternatives)) { + return; + } + + foreach ($aAlternatives as $i => $aAlternativeInfo) { + $sExtensionCode = $aAlternativeInfo["extension_code"] ?? null; + + if (in_array($sExtensionCode, $aExtensions)) { + $aStepRes = $this->ProcessSelectedOption($sCurrentIndex, $i, $aStepRes, $aAlternativeInfo, $aExtensions); + break; + } + } + } + + /** + * @param string $sCurrentIndex + * @param int|string $i + * @param array $aStepRes + * @param mixed $aOptionsInfo + * @param array $aExtensions + * + * @return array + */ + public function ProcessSelectedOption(string $sCurrentIndex, int|string $i, array $aStepRes, mixed $aOptionsInfo, array $aExtensions): array + { + $sNextIndex = "{$sCurrentIndex}_{$i}"; + $aStepRes[$sNextIndex] = $sNextIndex; + + $aSubOptions = $aOptionsInfo['sub_options'] ?? null; + if (!is_null($aSubOptions) && is_array($aSubOptions)) { + $this->ProcessOptions($sNextIndex, $aSubOptions, $aExtensions, $aStepRes); + $this->ProcessAlternatives($sNextIndex, $aSubOptions, $aExtensions, $aStepRes); + } + + $this->ProcessAlternatives($sNextIndex, $aOptionsInfo, $aExtensions, $aStepRes); + + return $aStepRes; + } + public function Display(SetupPage $oPage): void { $this->DisplayStep($oPage); @@ -861,7 +942,7 @@ EOF return 'Non-uninstallable extension missing'; } - if ($this->GetStepInfo(1 + $this->GetStepIndex()) === null && $this->IsDataAuditEnabled()) { + if ($this->GetStepInfo(1 + $this->GetStepIndex()) === null) { return 'Check compatibility'; } diff --git a/setup/wizardsteps/WizStepSummary.php b/setup/wizardsteps/WizStepSummary.php index 3d70e8ffaa..f8cefaa9dc 100644 --- a/setup/wizardsteps/WizStepSummary.php +++ b/setup/wizardsteps/WizStepSummary.php @@ -17,7 +17,6 @@ * * You should have received a copy of the GNU Affero General Public License */ -use Combodo\iTop\Application\WebPage\WebPage; /** * Summary of the installation tasks @@ -84,7 +83,7 @@ class WizStepSummary extends AbstractWizStepInstall $oPage->add('
'); $oPage->add('
Extensions to be installed'); - $aExtensionsAdded = json_decode($this->oWizard->GetParameter('extensions_added'), true); + $aExtensionsAdded = json_decode($this->oWizard->GetParameter('added_extensions'), true) ?? []; if (count($aExtensionsAdded) > 0) { $sExtensionsAdded = '
    '; diff --git a/setup/wizardsteps_autoload.php b/setup/wizardsteps_autoload.php index 743ba5dc9b..7ab7353fc7 100644 --- a/setup/wizardsteps_autoload.php +++ b/setup/wizardsteps_autoload.php @@ -18,4 +18,5 @@ require_once(APPROOT.'setup/wizardsteps/WizStepInstallMiscParams.php'); require_once(APPROOT.'setup/wizardsteps/WizStepModulesChoice.php'); require_once(APPROOT.'setup/wizardsteps/WizStepSummary.php'); require_once(APPROOT.'setup/wizardsteps/WizStepUpgradeMiscParams.php'); +require_once(APPROOT.'setup/wizardsteps/WizStepLandingBeforeAudit.php'); require_once(APPROOT.'setup/wizardcontroller.class.inc.php'); diff --git a/sources/Application/UI/Base/Component/Form/Form.php b/sources/Application/UI/Base/Component/Form/Form.php index 6242a18bb7..beba58c738 100644 --- a/sources/Application/UI/Base/Component/Form/Form.php +++ b/sources/Application/UI/Base/Component/Form/Form.php @@ -24,6 +24,8 @@ class Form extends UIContentBlock protected $sOnSubmitJsCode; /** @var string */ protected $sAction; + /** @var string */ + protected $sEncType = "multipart/form-data"; public function __construct(?string $sId = null) { @@ -65,4 +67,25 @@ class Form extends UIContentBlock return $this; } + /** + * Override default enctype (default : "multipart/form-data") + * @param string $sEncType + * @return $this + * @since 3.3.0 + */ + public function SetEncType(string $sEncType) + { + $this->sEncType = $sEncType; + return $this; + } + + /** +* @return string + * @since 3.3.0 + */ + public function GetEncType(): string + { + return $this->sEncType; + } + } diff --git a/templates/base/components/form/layout.html.twig b/templates/base/components/form/layout.html.twig index 756dee0924..7b2808e7ce 100644 --- a/templates/base/components/form/layout.html.twig +++ b/templates/base/components/form/layout.html.twig @@ -1,4 +1,4 @@ -
    AssertSummaryEquals($aExpected, $aRes); } + public function testExecuteCleanup_CheckSummaryIsEnrichedAfterEachPass() + { + $aExecutionSummary = []; + + $this->GivenDFRTreeInDB( + <<ExecuteCleanup($aClasses); + + $aExpected = [ + ['DFRToUpdate', 3, 0 ], + ['DFRToRemoveLeaf', 0, 3 ], + ['DFRRemovedCollateral', 0, 3 ], + ['DFRRemovedCollateralCascade', 0, 3 ], + ]; + $this->AssertSummaryEquals($aExpected, $aExecutionSummary); + + $this->GivenDFRTreeInDB( + <<ExecuteCleanup($aClasses); + + $aExpected = [ + ['DFRToUpdate', 1, 0, 0, 4 ], + ['DFRToRemoveLeaf', 0, 1, 0, 0, 4 ], + ['DFRRemovedCollateral', 0, 1, 0, 0, 4 ], + ['DFRRemovedCollateralCascade', 0, 1, 0, 0, 4 ], + ]; + $this->AssertSummaryEquals($aExpected, $aExecutionSummary); + } + public function testGetCleanupSummary_DeleteManyObjPerClassWithoutLimit() { $this->GivenDFRTreeInDB(<<iUpdateCount = $iUpdate; $oCleanupSummaryEntity->iDeleteCount = $iDelete; $oCleanupSummaryEntity->iIssueCount = $iIssue; + $oCleanupSummaryEntity->iTotalUpdateCount = $iTotalUpdate; + $oCleanupSummaryEntity->iTotalDeleteCount = $iTotalDelete; + $aExpected[$sClass] = $oCleanupSummaryEntity; } $this->assertEquals($aExpected, $actual, $sMessage); @@ -270,18 +328,18 @@ class DataCleanupServiceTest extends ItopCustomDatamodelTestCase private array $aIdByObjectName = []; private function GivenDFRTreeLineInDB(string $sLine) { - list($sLeft, $sRight) = explode('<-', $sLine); + [$sLeft, $sRight] = explode('<-', $sLine); $sLeft = trim($sLeft); $iLeftId = $this->aIdByObjectName[$sLeft] ?? 0; if ($iLeftId === 0) { - list($sChildClass, ) = explode('_', $sLeft, 2); + [$sChildClass, ] = explode('_', $sLeft, 2); $iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]); $this->aIdByObjectName[$sLeft] = $iLeftId; } $sRight = trim($sRight); - list($sChildClass, ) = explode('_', $sRight, 2); + [$sChildClass, ] = explode('_', $sRight, 2); $iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sRight, 'extkey_id' => $iLeftId]); $this->aIdByObjectName[$sRight] = $iRightId; } diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php new file mode 100644 index 0000000000..179bea189c --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php @@ -0,0 +1,125 @@ + "combodo-data-feature-removal", + "exec_page" => "index.php", + 'exec_env' => 'production', +]; + +new ContextTag(ContextTag::TAG_SETUP); +$sToken = SetupUtils::CreateSetupToken(); + +$aSelectedModules = [ + 'authent-cas', + 'authent-external', + 'authent-ldap', + 'authent-local', + 'combodo-backoffice-darkmoon-theme', + 'combodo-backoffice-fullmoon-high-contrast-theme', + 'combodo-backoffice-fullmoon-protanopia-deuteranopia-theme', + 'combodo-backoffice-fullmoon-tritanopia-theme', + 'combodo-data-feature-removal', + 'itop-backup', + 'itop-config', + 'itop-files-information', + 'itop-portal-base', + 'itop-profiles-itil', + 'itop-sla-computation', + 'itop-structure', + 'itop-welcome-itil', + 'itop-config-mgmt', + 'itop-attachments', + 'itop-tickets', + 'combodo-db-tools', + 'itop-core-update', + 'itop-hub-connector', + 'itop-oauth-client', + 'itop-themes-compat', + 'combodo-my-account', + 'combodo-my-account-user-info', + 'combodo-oauth2-client', + 'itop-attribute-class-set', + 'itop-attribute-encrypted-password', + 'itop-ui-copypaste', + 'itop-datacenter-mgmt', + 'itop-endusers-devices', + 'itop-storage-mgmt', + 'itop-virtualization-mgmt', + 'itop-bridge-cmdb-ticket', + 'itop-bridge-virtualization-storage', + 'itop-service-mgmt', + 'itop-bridge-cmdb-services', + 'itop-bridge-datacenter-mgmt-services', + 'itop-bridge-endusers-devices-services', + 'itop-bridge-storage-mgmt-services', + 'itop-bridge-virtualization-mgmt-services', + 'itop-request-mgmt', + 'itop-portal', + 'itop-change-mgmt', + 'itop-faq-light', + 'itop-knownerror-mgmt', + 'itop-problem-mgmt', + 'itop-system-information', + 'itop-log-mgmt', +]; + +$aSelectedExtensions = [ + 'itop-config-mgmt-core', + 'itop-config-mgmt-datacenter', + 'itop-config-mgmt-end-user', + 'itop-config-mgmt-storage', + 'itop-config-mgmt-virtualization', + 'itop-service-mgmt-enterprise', + 'itop-ticket-mgmt-simple-ticket', + 'itop-ticket-mgmt-simple-ticket-enhanced-portal', + 'itop-change-mgmt-simple', + 'itop-kown-error-mgmt', + 'itop-problem-mgmt', + 'itop-system-information', + 'itop-log-mgmt', +]; + +$aRemovedExtensions = ['itop-container-mgmt' => 'Containerization']; + +$aPostParams = [ + "auth_user" => 'admin', + "auth_pwd" => 'admin', + 'login_mode' => 'form', + 'operation' => 'AnalysisResult', + 'setup_token' => $sToken, + 'selected_modules' => utils::HtmlEntities(json_encode($aSelectedModules)), + 'selected_extensions' => utils::HtmlEntities(json_encode($aSelectedExtensions)), + 'removed_extensions' => utils::HtmlEntities(json_encode($aRemovedExtensions)), +]; + +$sHiddenPostedInput = ""; +foreach ($aPostParams as $sKey => $sVal) { + $sHiddenPostedInput .= << +INPUT; +} + +$sRedirectURL = utils::GetAbsoluteUrlModulePage('combodo-data-feature-removal', 'index.php'); + +$sDiv = << +DIV; + +$sReadyJs = <<add($sDiv); +$oP->add_ready_script($sReadyJs); +$oP->output(); diff --git a/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php b/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php index 3f505b6fe4..0dec4dc9d5 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ExtensionsMapTest.php @@ -151,4 +151,35 @@ class ExtensionsMapTest extends ItopTestCase $this->SetNonPublicProperty($oExtensionsMap, $mapKeyInItopExtensionMap, $aMap); } + public function testiTopExtensionsMapInit() + { + $oiTopExtensionsMap = new iTopExtensionsMap(sAppRootForTests:__DIR__."/ressources"); + + //file_put_contents(__DIR__.'/ressources/all_extensions_from_datamodels.json', json_encode($this->SerializeExtensionMap($oiTopExtensionsMap), JSON_PRETTY_PRINT)); + + $sExpected = file_get_contents(__DIR__.'/ressources/all_extensions_from_datamodels.json'); + $sExpected = str_replace('"sVersion": "ITOP_VERSION"', '"sVersion": "'.ITOP_VERSION.'"', $sExpected); + $sExpected = preg_replace('/"module_file_path": .*/', '"module_file_path": ANYPATH', $sExpected); + + $actual = json_encode($this->SerializeExtensionMap($oiTopExtensionsMap), JSON_PRETTY_PRINT); + $actual = preg_replace('/"module_file_path": .*/', '"module_file_path": ANYPATH', $actual); + $this->assertEquals($sExpected, $actual); + } + + public function SerializeExtensionMap(iTopExtensionsMap $oiTopExtensionsMap): array + { + $aRes = []; + foreach ($oiTopExtensionsMap->GetAllExtensions() as $oExtension) { + $aRes[] = [ + 'sCode' => $oExtension->sCode, + 'sSource' => $oExtension->sSource, + 'sVersion' => $oExtension->sVersion, + 'aModules' => $oExtension->aModules, + 'aModuleVersion' => $oExtension->aModuleVersion, + 'aModuleInfo' => $oExtension->aModuleInfo, + ]; + } + + return $aRes; + } } diff --git a/tests/php-unit-tests/unitary-tests/setup/WizStepModulesChoiceTest.php b/tests/php-unit-tests/unitary-tests/setup/WizStepModulesChoiceTest.php index 04429c87d2..d04cb0221c 100644 --- a/tests/php-unit-tests/unitary-tests/setup/WizStepModulesChoiceTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/WizStepModulesChoiceTest.php @@ -1391,4 +1391,16 @@ HTML, $this->assertEquals($sExpectedHTML, $oPage->sContent); } + + public function testGetSelectedComponents() + { + $aParams = new XMLParameters(__DIR__.'/ressources/installation_330.xml'); + $aSteps = $aParams->Get('steps', []); + + $aSelectedExtensions = ["itop-config-mgmt-core","itop-config-mgmt-datacenter","itop-config-mgmt-end-user","itop-config-mgmt-storage","itop-config-mgmt-virtualization","itop-container-mgmt","itop-service-mgmt-enterprise","itop-ticket-mgmt-simple-ticket","itop-ticket-mgmt-simple-ticket-enhanced-portal","itop-change-mgmt-simple","itop-kown-error-mgmt","itop-problem-mgmt","combodo-oauth2-client","combodo-mfa-extended","combodo-data-replication","combodo-api-playground","combodo-snapshot"]; + $aRes = $this->oStep->GetSelectedComponents($aSteps, json_encode($aSelectedExtensions)); + + $aExpected = json_decode('[{"_0":"_0","_1":"_1","_2":"_2","_3":"_3","_4":"_4","_4_0":"_4_0"},{"_0":"_0"},{"_0":"_0","_0_0":"_0_0"},{"_0":"_0"},{"_0":"_0","_1":"_1"}]', true); + $this->assertEquals($aExpected, $aRes); + } } diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/all_extensions_from_datamodels.json b/tests/php-unit-tests/unitary-tests/setup/ressources/all_extensions_from_datamodels.json new file mode 100644 index 0000000000..68531962d5 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/all_extensions_from_datamodels.json @@ -0,0 +1,316 @@ +[ + { + "sCode": "itop-config-mgmt-core", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-structure", + "itop-config-mgmt", + "itop-attachments", + "itop-profiles-itil", + "itop-welcome-itil", + "itop-tickets", + "itop-files-information", + "combodo-db-tools", + "itop-core-update", + "itop-hub-connector", + "itop-oauth-client", + "combodo-backoffice-darkmoon-theme", + "combodo-backoffice-fullmoon-high-contrast-theme", + "combodo-backoffice-fullmoon-protanopia-deuteranopia-theme", + "combodo-backoffice-fullmoon-tritanopia-theme", + "itop-themes-compat", + "combodo-my-account", + "combodo-my-account-user-info", + "combodo-oauth2-client", + "itop-attribute-class-set", + "itop-attribute-encrypted-password", + "itop-ui-copypaste" + ], + "aModuleVersion": { + "itop-structure": "3.3.0", + "itop-config-mgmt": "3.3.0" + }, + "aModuleInfo": { + "itop-structure": { + "label": "Core iTop Structure", + "category": "business", + "dependencies": [], + "mandatory": true, + "visible": false, + "installer": "StructureInstaller", + "datamodel": [ + "main.itop-structure.php" + ], + "data.struct": [], + "data.sample": [ + "data\/data.sample.organizations.xml", + "data\/data.sample.locations.xml", + "data\/data.sample.persons.xml", + "data\/data.sample.teams.xml", + "data\/data.sample.contactteam.xml", + "data\/data.sample.contacttype.xml" + ], + "doc.manual_setup": "", + "doc.more_information": "", + "settings": [], + "module_file_path": "APPROOTtests\/php-unit-tests\/unitary-tests\/setup\/ressources\/datamodels\/2.x\/itop-structure\/module.itop-structure.php" + }, + "itop-config-mgmt": { + "label": "Configuration Management (CMDB)", + "category": "business", + "dependencies": [ + "itop-structure\/2.7.1" + ], + "mandatory": false, + "visible": true, + "installer": "ConfigMgmtInstaller", + "datamodel": [ + "model.itop-config-mgmt.php", + "main.itop-config-mgmt.php" + ], + "data.struct": [ + "data\/en_us.data.itop-brand.xml", + "data\/en_us.data.itop-networkdevicetype.xml", + "data\/en_us.data.itop-osfamily.xml", + "data\/en_us.data.itop-osversion.xml" + ], + "data.sample": [ + "data\/data.sample.model.xml", + "data\/data.sample.networkdevicetype.xml", + "data\/data.sample.servers.xml", + "data\/data.sample.nw-devices.xml", + "data\/data.sample.software.xml", + "data\/data.sample.dbserver.xml", + "data\/data.sample.dbschema.xml", + "data\/data.sample.webserver.xml", + "data\/data.sample.webapp.xml", + "data\/data.sample.applications.xml", + "data\/data.sample.applicationsolutionci.xml" + ], + "doc.manual_setup": "", + "doc.more_information": "", + "settings": [], + "module_file_path": "APPROOT\/tests\/php-unit-tests\/unitary-tests\/setup\/ressources\/datamodels\/2.x\/itop-config-mgmt\/module.itop-config-mgmt.php" + } + } + }, + { + "sCode": "itop-config-mgmt-datacenter", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-datacenter-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-config-mgmt-end-user", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-endusers-devices" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-config-mgmt-storage", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-storage-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-container-mgmt", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-container-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-config-mgmt-virtualization", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-virtualization-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-service-mgmt-enterprise", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-service-mgmt" + ], + "aModuleVersion": { + "itop-service-mgmt": "3.3.0" + }, + "aModuleInfo": { + "itop-service-mgmt": { + "label": "Service Management", + "category": "business", + "dependencies": [ + "itop-tickets\/2.0.0" + ], + "mandatory": false, + "visible": true, + "installer": "ServiceMgmtInstaller", + "datamodel": [], + "data.struct": [], + "data.sample": [ + "data\/data.sample.organizations.xml", + "data\/data.sample.contracts.xml", + "data\/data.sample.servicefamilies.xml", + "data\/data.sample.services.xml", + "data\/data.sample.serviceelements.xml", + "data\/data.sample.sla.xml", + "data\/data.sample.slt.xml", + "data\/data.sample.sltsla.xml", + "data\/data.sample.contractservice.xml", + "data\/data.sample.deliverymodelcontact.xml" + ], + "doc.manual_setup": "", + "doc.more_information": "", + "settings": [], + "module_file_path": "APPROOT\/tests\/php-unit-tests\/unitary-tests\/setup\/ressources\/datamodels\/2.x\/itop-service-mgmt\/module.itop-service-mgmt.php" + } + } + }, + { + "sCode": "itop-service-mgmt-service-provider", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-service-mgmt-provider" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-simple-ticket-enhanced-portal", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-portal", + "itop-portal-base" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-simple-ticket", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-request-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-itil-user-request", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-request-mgmt-itil" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-itil-incident", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-incident-mgmt-itil" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-itil-enhanced-portal", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-portal", + "itop-portal-base" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-itil", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-ticket-mgmt-none", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-change-mgmt-simple", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-change-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-change-mgmt-itil", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-change-mgmt-itil" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-change-mgmt-none", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-kown-error-mgmt", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-faq-light", + "itop-knownerror-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + }, + { + "sCode": "itop-problem-mgmt", + "sSource": "datamodels", + "sVersion": "ITOP_VERSION", + "aModules": [ + "itop-problem-mgmt" + ], + "aModuleVersion": [], + "aModuleInfo": [] + } +] \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/installation.xml b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/installation.xml new file mode 100644 index 0000000000..70d3866c17 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/installation.xml @@ -0,0 +1,243 @@ + + + + + Configuration Management options + The options below allow you to configure the type of elements that are to be managed inside iTop.]]> + /images/icons/icons8-apps-tab.svg + + + itop-config-mgmt-core + Configuration Management Core + All the base objects that are mandatory in the iTop CMDB: Organizations, Locations, Teams, Persons, etc. + + itop-structure + itop-config-mgmt + itop-attachments + itop-profiles-itil + itop-welcome-itil + itop-tickets + itop-files-information + combodo-db-tools + itop-core-update + itop-hub-connector + itop-oauth-client + combodo-backoffice-darkmoon-theme + combodo-backoffice-fullmoon-high-contrast-theme + combodo-backoffice-fullmoon-protanopia-deuteranopia-theme + combodo-backoffice-fullmoon-tritanopia-theme + itop-themes-compat + combodo-my-account + combodo-my-account-user-info + combodo-oauth2-client + itop-attribute-class-set + itop-attribute-encrypted-password + itop-ui-copypaste + + true + + + itop-config-mgmt-datacenter + Data Center Devices + Manage Data Center devices such as Racks, Enclosures, PDUs, etc. + + itop-datacenter-mgmt + + true + + + itop-config-mgmt-end-user + End-User Devices + Manage devices related to end-users: PCs, Phones, Tablets, etc. + + itop-endusers-devices + + true + + + itop-config-mgmt-storage + Storage Devices + Manage storage devices such as NAS, SAN Switches, Tape Libraries and Tapes, etc. + + itop-storage-mgmt + + true + + + itop-config-mgmt-virtualization + Virtualization + Manage Hypervisors, Virtual Machines and Farms. + + itop-virtualization-mgmt + + true + + + + itop-container-mgmt + Containerization + + + itop-container-mgmt + + false + + + + + + + + Service Management options + Select the choice that best describes the relationships between the services and the IT infrastructure in your IT environment.]]> + /images/icons/icons8-services.svg + + + itop-service-mgmt-enterprise + Service Management for Enterprises + Select this option if the IT delivers services based on a shared infrastructure. For example if different organizations within your company subscribe to services (like Mail and Print services) delivered by a single shared backend. + + itop-service-mgmt + + true + + + itop-service-mgmt-service-provider + Service Management for Service Providers + Select this option if the IT manages the infrastructure of independent customers. This is the most flexible model, since the services can be delivered with a mix of shared and customer specific infrastructure devices. + + itop-service-mgmt-provider + + + + + + Tickets Management options + Select the type of tickets you want to use in order to respond to user requests and incidents.]]> + /images/icons/icons8-discussion-forum.svg + + + itop-ticket-mgmt-simple-ticket + Simple Ticket Management + Select this option to use one single type of tickets for all kind of requests. + + itop-request-mgmt + + true + + + + itop-ticket-mgmt-simple-ticket-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-itil + ITIL Compliant Tickets Management + Select this option to have different types of ticket for managing user requests and incidents. Each type of ticket has a specific life cycle and specific fields + + + + itop-ticket-mgmt-itil-user-request + User Request Management + Manage User Request tickets in iTop + + itop-request-mgmt-itil + + + + itop-ticket-mgmt-itil-incident + Incident Management + Manage Incidents tickets in iTop + + itop-incident-mgmt-itil + + + + itop-ticket-mgmt-itil-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-none + No Tickets Management + Don't manage incidents or user requests in iTop + + + + + + + Change Management options + Select the type of tickets you want to use in order to manage changes to the IT infrastructure.]]> + /images/icons/icons8-change.svg + + + itop-change-mgmt-simple + Simple Change Management + Select this option to use one type of ticket for all kind of changes. + + itop-change-mgmt + + true + + + itop-change-mgmt-itil + ITIL Change Management + Select this option to use Normal/Routine/Emergency change tickets. + + itop-change-mgmt-itil + + + + itop-change-mgmt-none + No Change Management + Don't manage changes in iTop + + + + + + + Additional ITIL tickets + Pick from the list below the additional ITIL processes that are to be implemented in iTop.]]> + /images/icons/icons8-important-book.svg + + + + itop-kown-error-mgmt + Known Errors Management and FAQ + Select this option to track "Known Errors" and FAQs in iTop. + + itop-faq-light + itop-knownerror-mgmt + + + + itop-problem-mgmt + Problem Management + Select this option to track "Problems" in iTop. + + itop-problem-mgmt + + + + + + diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php new file mode 100644 index 0000000000..12bea24bd1 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-config-mgmt/module.itop-config-mgmt.php @@ -0,0 +1,107 @@ + 'Configuration Management (CMDB)', + 'category' => 'business', + + // Setup + // + 'dependencies' => [ + 'itop-structure/2.7.1', + ], + 'mandatory' => false, + 'visible' => true, + 'installer' => 'ConfigMgmtInstaller', + + // Components + // + 'datamodel' => [ + 'model.itop-config-mgmt.php', + 'main.itop-config-mgmt.php', + ], + 'data.struct' => [ + 'data/en_us.data.itop-brand.xml', + 'data/en_us.data.itop-networkdevicetype.xml', + 'data/en_us.data.itop-osfamily.xml', + 'data/en_us.data.itop-osversion.xml', + ], + 'data.sample' => [ + 'data/data.sample.model.xml', + 'data/data.sample.networkdevicetype.xml', + 'data/data.sample.servers.xml', + 'data/data.sample.nw-devices.xml', + 'data/data.sample.software.xml', + 'data/data.sample.dbserver.xml', + 'data/data.sample.dbschema.xml', + 'data/data.sample.webserver.xml', + 'data/data.sample.webapp.xml', + 'data/data.sample.applications.xml', + 'data/data.sample.applicationsolutionci.xml', + ], + + // Documentation + // + 'doc.manual_setup' => '', + 'doc.more_information' => '', + + // Default settings + // + 'settings' => [ + ], + ] +); + +if (!class_exists('ConfigMgmtInstaller')) { + // Module installation handler + // + class ConfigMgmtInstaller extends ModuleInstallerAPI + { + public static function BeforeWritingConfig(Config $oConfiguration) + { + // If you want to override/force some configuration values, do it here + return $oConfiguration; + } + + /** + * Handler called before creating or upgrading the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function BeforeDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + if (strlen($sPreviousVersion) > 0) { + // If you want to migrate data from one format to another, do it here + self::RenameEnumValueInDB('Software', 'type', 'DBserver', 'DBServer'); + self::RenameEnumValueInDB('Software', 'type', 'Webserver', 'WebServer'); + self::RenameEnumValueInDB('Model', 'type', 'SANswitch', 'SANSwitch'); + self::RenameEnumValueInDB('Model', 'type', 'IpPhone', 'IPPhone'); + self::RenameEnumValueInDB('Model', 'type', 'Telephone', 'Phone'); + self::RenameClassInDB('DBserver', 'DBServer'); + self::RenameClassInDB('OSfamily', 'OSFamily'); + self::RenameClassInDB('OSversion', 'OSVersion'); + self::RenameClassInDB('Webserver', 'WebServer'); + self::RenameClassInDB('OSpatch', 'OSPatch'); + self::RenameClassInDB('lnkFunctionalCIToOSpatch', 'lnkFunctionalCIToOSPatch'); + self::RenameClassInDB('OsLicence', 'OSLicence'); + self::RenameClassInDB('IOSversion', 'IOSVersion'); + self::RenameClassInDB('IPinterface', 'IPInterface'); + } + } + + /** + * Handler called after the creation/update of the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + } + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php new file mode 100644 index 0000000000..98103251e5 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-service-mgmt/module.itop-service-mgmt.php @@ -0,0 +1,89 @@ + 'Service Management', + 'category' => 'business', + + // Setup + // + 'dependencies' => [ + 'itop-tickets/2.0.0', + ], + 'mandatory' => false, + 'visible' => true, + 'installer' => 'ServiceMgmtInstaller', + + // Components + // + 'datamodel' => [ + ], + 'data.struct' => [ + //'data.struct.itop-service-mgmt.xml', + ], + 'data.sample' => [ + 'data/data.sample.organizations.xml', + 'data/data.sample.contracts.xml', + 'data/data.sample.servicefamilies.xml', + 'data/data.sample.services.xml', + 'data/data.sample.serviceelements.xml', + 'data/data.sample.sla.xml', + 'data/data.sample.slt.xml', + 'data/data.sample.sltsla.xml', + // 'data/data.sample.coveragewindows.xml', + 'data/data.sample.contractservice.xml', + // 'data/data.sample.deliverymodel.xml', + 'data/data.sample.deliverymodelcontact.xml', + ], + + // Documentation + // + 'doc.manual_setup' => '', + 'doc.more_information' => '', + + // Default settings + // + 'settings' => [ + ], + ] +); + +if (!class_exists('ServiceMgmtInstaller')) { + // Module installation handler + // + class ServiceMgmtInstaller extends ModuleInstallerAPI + { + public static function BeforeWritingConfig(Config $oConfiguration) + { + // If you want to override/force some configuration values, do it here + return $oConfiguration; + } + + /** + * Handler called before creating or upgrading the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function BeforeDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + if (strlen($sPreviousVersion) > 0) { + self::RenameEnumValueInDB('SLT', 'request_type', 'servicerequest', 'service_request'); + } + } + + /** + * Handler called after the creation/update of the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + } + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-structure/module.itop-structure.php b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-structure/module.itop-structure.php new file mode 100644 index 0000000000..f1da9aeafe --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/datamodels/2.x/itop-structure/module.itop-structure.php @@ -0,0 +1,449 @@ + 'Core iTop Structure', + 'category' => 'business', + + // Setup + // + 'dependencies' => [ + ], + 'mandatory' => true, + 'visible' => false, + 'installer' => 'StructureInstaller', + + // Components + // + 'datamodel' => [ + 'main.itop-structure.php', + ], + 'data.struct' => [ + ], + 'data.sample' => [ + 'data/data.sample.organizations.xml', + 'data/data.sample.locations.xml', + 'data/data.sample.persons.xml', + 'data/data.sample.teams.xml', + 'data/data.sample.contactteam.xml', + 'data/data.sample.contacttype.xml', + ], + + // Documentation + // + 'doc.manual_setup' => '', + 'doc.more_information' => '', + + // Default settings + // + 'settings' => [ + ], + ] +); + +if (!class_exists('StructureInstaller')) { + // Module installation handler + // + class StructureInstaller extends ModuleInstallerAPI + { + public static function BeforeWritingConfig(Config $oConfiguration) + { + // If you want to override/force some configuration values, do it here + return $oConfiguration; + } + + /** + * Handler called before creating or upgrading the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function BeforeDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + if (strlen($sPreviousVersion) > 0) { + // Search for existing ActionEmail where the language attribute was defined on its child + if (version_compare($sPreviousVersion, '3.2.0', '<')) { + SetupLog::Info("| Migrate ActionEmail language attribute values to its parent."); + $sTableToRead = MetaModel::DBGetTable('ActionEmail'); + $sTableToSet = MetaModel::DBGetTable('ActionNotification'); + self::MoveColumnInDB($sTableToRead, 'language', $sTableToSet, 'language', true); + SetupLog::Info("| ActionEmail migration done."); + } + // If you want to migrate data from one format to another, do it here + self::RenameEnumValueInDB('Software', 'type', 'DBserver', 'DBServer'); + self::RenameEnumValueInDB('Software', 'type', 'Webserver', 'WebServer'); + self::RenameEnumValueInDB('Model', 'type', 'SANswitch', 'SANSwitch'); + self::RenameEnumValueInDB('Model', 'type', 'IpPhone', 'IPPhone'); + self::RenameEnumValueInDB('Model', 'type', 'Telephone', 'Phone'); + self::RenameClassInDB('DBserver', 'DBServer'); + self::RenameClassInDB('OSfamily', 'OSFamily'); + self::RenameClassInDB('OSversion', 'OSVersion'); + self::RenameClassInDB('Webserver', 'WebServer'); + self::RenameClassInDB('OSpatch', 'OSPatch'); + self::RenameClassInDB('lnkFunctionalCIToOSpatch', 'lnkFunctionalCIToOSPatch'); + self::RenameClassInDB('OsLicence', 'OSLicence'); + self::RenameClassInDB('IOSversion', 'IOSVersion'); + self::RenameClassInDB('IPinterface', 'IPInterface'); + } + } + + /** + * Handler called after the creation/update of the database schema + * @param $oConfiguration Config The new configuration of the application + * @param $sPreviousVersion string PRevious version number of the module (empty string in case of first install) + * @param $sCurrentVersion string Current version number of the module + */ + public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion) + { + // Default language will be used for actions + // Note: There is a issue when upgrading, default language cannot be retrieved from the passed configuration, we have to read it from the disk + if (utils::IsNullOrEmptyString($sPreviousVersion)) { + // Fresh install + $sDefaultLanguage = $oConfiguration->GetDefaultLanguage(); + } else { + // Upgrade + $sDefaultLanguage = utils::GetConfig(true)->GetDefaultLanguage(); + } + // Fallback language on english if not french + $sDefaultLanguage = $sDefaultLanguage === 'FR FR' ? 'FR FR' : 'EN US'; + SetupLog::Info("Default app language used for actions: $sDefaultLanguage"); + + // Search for existing TriggerOnObject where the Trigger string complement is empty and fed it with target_class field value + if (version_compare($sPreviousVersion, '3.1.0', '<')) { + SetupLog::Info("| Feed computed field triggering_class on existing Triggers."); + + $sTableToSet = MetaModel::DBGetTable('Trigger', 'complement'); + $sTableToRead = MetaModel::DBGetTable('TriggerOnObject', 'target_class'); + $oAttDefToSet = MetaModel::GetAttributeDef('Trigger', 'complement'); + $oAttDefToRead = MetaModel::GetAttributeDef('TriggerOnObject', 'target_class'); + + $aColumnsToSets = array_keys($oAttDefToSet->GetSQLColumns()); + $sColumnToSet = $aColumnsToSets[0]; // We know that a string has only one column + $aColumnsToReads = array_keys($oAttDefToRead->GetSQLColumns()); + $sColumnToRead = $aColumnsToReads[0]; // We know that a string has only one column + + $sRepair = "UPDATE $sTableToSet JOIN $sTableToRead ON $sTableToSet.id = $sTableToRead.id SET $sTableToSet.$sColumnToSet = CONCAT('class restriction: ',$sTableToRead.$sColumnToRead) WHERE $sTableToSet.$sColumnToSet = ''"; + SetupLog::Debug(" | | Query: ".$sRepair); + CMDBSource::Query($sRepair); + $iNbProcessed = CMDBSource::AffectedRows(); + SetupLog::Info("| | ".$iNbProcessed." triggers processed."); + } + + // Add notifications by email to Persons if mentioned on any log + if (version_compare($sPreviousVersion, '3.0.0', '<')) { + SetupLog::Info("Adding default triggers/action for Person objects mentions. All DM classes with at least 1 log attribute will be concerned..."); + + $sPersonClass = 'Person'; + $sPersonStateAttCode = MetaModel::GetStateAttributeCode($sPersonClass); + $sPersonOwnerOrgAttCode = UserRightsProfile::GetOwnerOrganizationAttCode($sPersonClass); + + $iClassesWithLogCount = 0; + $aCreatedTriggerIds = []; + foreach (MetaModel::EnumRootClasses() as $sRootClass) { + foreach (MetaModel::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL, true) as $sClass) { + $aLogAttCodes = MetaModel::GetAttributesList($sClass, ['AttributeCaseLog']); + + // Skip class with no log attribute + if (count($aLogAttCodes) === 0) { + continue; + } + + // Prepare the mentioned_filter OQL + $oPersonSearch = DBObjectSearch::FromOQL("SELECT $sPersonClass"); + + // - Add status condition if attribute present + if (empty($sPersonStateAttCode) === false) { + $oPersonSearch->AddConditionExpression(new BinaryExpression( + new FieldExpression($sPersonStateAttCode), + '=', + new ScalarExpression('active') + )); + } + + // - Check if the classes have a silo attribute so we can use them in the mentioned_filter + if (empty($sPersonOwnerOrgAttCode) === false) { + // Filter on current contact org. + $oCurrentContactExpr = new BinaryExpression( + new FieldExpression($sPersonOwnerOrgAttCode), + '=', + new VariableExpression("current_contact->org_id") + ); + + // Filter on class owner org. if any + $sClassOwnerOrgAttCode = UserRightsProfile::GetOwnerOrganizationAttCode($sClass); + $oOwnerOrgExpr = empty($sClassOwnerOrgAttCode) ? null : new BinaryExpression( + new FieldExpression($sPersonOwnerOrgAttCode), + '=', + new VariableExpression("this->$sClassOwnerOrgAttCode") + ); + + // No owner org, simple condition + if ($oOwnerOrgExpr === null) { + $oPersonSearch->AddConditionExpression($oCurrentContactExpr); + } + // Owner org, condition is either from owner org or current contact's + else { + $oOrExpr = new BinaryExpression($oCurrentContactExpr, 'OR', $oOwnerOrgExpr); + $oPersonSearch->AddConditionExpression($oOrExpr); + } + } + + // Build the trigger + $oTrigger = MetaModel::NewObject(TriggerOnObjectMention::class); + $oTrigger->Set('description', 'Person mentioned on '.$sClass); + $oTrigger->Set('target_class', $sClass); + $oTrigger->Set('mentioned_filter', $oPersonSearch->ToOQL()); + $oTrigger->DBInsert(); + + SetupLog::Info("|- Created trigger \"{$oTrigger->Get('description')}\" for class $sClass."); + $aCreatedTriggerIds[] = $oTrigger->GetKey(); + $iClassesWithLogCount++; + // Note: We break because we only have to create one trigger/action for the class hierarchy as it will be for all their log attributes + break; + } + } + + // Build the corresponding action and link it to the triggers + if (count($aCreatedTriggerIds) > 0) { + // Actions data for english and french + $aActionsData = [ + 'EN US' => [ + 'name' => 'Notification to persons mentioned in logs', + 'subject' => 'You have been mentioned in "$this->friendlyname$"', + 'body' => '

    Hello $mentioned->first_name$,

    +

    You have been mentioned by $current_contact->friendlyname$ in $this->hyperlink()$

    ', + ], + 'FR FR' => [ + 'name' => 'Notification aux personnes mentionnées dans les journaux', + 'subject' => 'Vous avez été mentionné dans "$this->friendlyname$"', + 'body' => '

    Bonjour $mentioned->first_name$,

    +

    Vous avez été mentionné par $current_contact->friendlyname$ dans $this->hyperlink()$

    ', + ], + ]; + + // Create action in app. default language and link it to the triggers + $aData = $aActionsData[$sDefaultLanguage]; + $oAction = MetaModel::NewObject(ActionEmail::class); + $oAction->Set('name', $aData['name']); + $oAction->Set('status', 'enabled'); + $oAction->Set('language', $sDefaultLanguage); + $oAction->Set('from', '$current_contact->email$'); + $oAction->Set('to', 'SELECT Person WHERE id = :mentioned->id'); + $oAction->Set('subject', $aData['subject']); + $oAction->Set('body', $aData['body']); + + /** @var \ormLinkSet $oOrm */ + $oOrm = $oAction->Get('trigger_list'); + foreach ($aCreatedTriggerIds as $sTriggerId) { + $oLink = new lnkTriggerAction(); + $oLink->Set('trigger_id', $sTriggerId); + $oOrm->AddItem($oLink); + } + $oAction->Set('trigger_list', $oOrm); + $oAction->DBInsert(); + + SetupLog::Info("|- Created action \"{$oAction->Get('name')}\" and linked it to the previously created triggers."); + } + + if ($iClassesWithLogCount === 0) { + SetupLog::Info("... no trigger/action created as there is no DM class with a log attribute."); + } else { + SetupLog::Info("... default triggers/action successfully created for $iClassesWithLogCount classes."); + } + } + + // Add notifications by newsroom to Persons if mentioned on any log + if (version_compare($sPreviousVersion, '3.2.0', '<')) { + SetupLog::Info("Adding default newsroom actions for Person objects mentions. All existing TriggerOnObjectMention mentioning the Person class will be concerned..."); + + $sPersonClass = Person::class; + $iExistingTriggersCount = 0; + + // Actions data for english and french + $aActionsData = [ + 'EN US' => [ + 'name' => 'Notification to persons mentioned in logs', + 'message' => 'You have been mentioned by $current_contact->friendlyname$', + ], + 'FR FR' => [ + 'name' => 'Notification aux personnes mentionnées dans les journaux', + 'message' => 'Vous avez été mentionné par $current_contact->friendlyname$', + ], + ]; + + // Start by creating the default action no matter what (even if there is no relevant trigger, it will be there for future use) + $aData = $aActionsData[$sDefaultLanguage]; + $oAction = MetaModel::NewObject(ActionNewsroom::class); + $oAction->Set('name', $aData['name']); + $oAction->Set('status', 'enabled'); + $oAction->Set('language', $sDefaultLanguage); + $oAction->Set('priority', 3); // Important priority as a mention is probably more important than a simple notification + $oAction->Set('recipients', 'SELECT Person WHERE id = :mentioned->id'); + $oAction->Set('title', '$this->friendlyname$'); + $oAction->Set('message', $aData['message']); + $oAction->DBWrite(); + + SetupLog::Info("|- Created newsroom action \"{$oAction->Get('name')}\"."); + + // Retrieve all triggers and find those with a mentioned_filter on the Person class + $oTriggersSearch = DBObjectSearch::FromOQL("SELECT ".TriggerOnObjectMention::class); + $oTriggersSearch->AllowAllData(); + + $oTriggersSet = new DBObjectSet($oTriggersSearch); + while ($oTrigger = $oTriggersSet->Fetch()) { + // If mentioned class is not a Person, ignore + $oMentionedFilter = DBSearch::FromOQL($oTrigger->Get('mentioned_filter')); + if (!is_null($oMentionedFilter) && is_a($oMentionedFilter->GetClass(), $sPersonClass, true) === false) { + SetupLog::Info("|- Action \"{$oAction->GetName()}\" NOT LINKED to existing trigger \"{$oTrigger->GetName()}\". (mentioned class \"{$oMentionedFilter->GetClass()}\")"); + continue; + } + + // Link the trigger to the action + /** @var \ormLinkSet $oOrm */ + $oOrm = $oTrigger->Get('action_list'); + $oLink = new lnkTriggerAction(); + $oLink->Set('action_id', $oAction->GetKey()); + $oOrm->AddItem($oLink); + + $oTrigger->Set('action_list', $oOrm); + $oTrigger->DBUpdate(); + $iExistingTriggersCount++; + + SetupLog::Info("|- Linked newsroom action \"{$oAction->GetName()}\" to existing trigger \"{$oTrigger->GetName()}\"."); + } + + if ($iExistingTriggersCount === 0) { + SetupLog::Info("... no action created as there is no existing trigger on mention for the $sPersonClass class."); + } else { + SetupLog::Info("... default newsroom action successfully created and linked to $iExistingTriggersCount triggers on mention."); + } + } + + // Force subscription policy to ForceAtLeastOneChannel for all existing TriggerOnObjectMention + if (version_compare($sPreviousVersion, '3.2.0', '<')) { + SetupLog::Info("Forcing subscription policy to ForceAtLeastOneChannel for all existing TriggerOnObjectMention..."); + + $oTriggersSearch = DBObjectSearch::FromOQL("SELECT ".TriggerOnObjectMention::class); + $oTriggersSearch->AllowAllData(); + + $oTriggersSet = new DBObjectSet($oTriggersSearch); + while ($oTrigger = $oTriggersSet->Fetch()) { + $oTrigger->Set('subscription_policy', \Combodo\iTop\Core\Trigger\Enum\SubscriptionPolicy::ForceAtLeastOneChannel->value); + $oTrigger->DBUpdate(); + + SetupLog::Info("|- Trigger \"{$oTrigger->GetName()}\" updated."); + } + + SetupLog::Info("... all existing TriggerOnObjectMention updated."); + } + + // Add notifications by newsroom (not linked to any trigger yet) for TriggerOnPortalUpdate and TriggerOnReachingState + if (version_compare($sPreviousVersion, '3.2.0', '<')) { + // TriggerOnPortalUpdate + SetupLog::Info("Adding default newsroom action for TriggerOnPortalUpdate (not linked to any trigger yet)..."); + + // - Actions data for english and french + $aActionsData = [ + 'EN US' => [ + 'name' => 'Notification on public log update through the portal', + 'message' => 'New message from $current_contact->friendlyname$', + ], + 'FR FR' => [ + 'name' => 'Notification sur MAJ du journal public via le portail', + 'message' => 'Nouveau message de $current_contact->friendlyname$', + ], + ]; + + // - Create action in app. default language and link it to the triggers + $aData = $aActionsData[$sDefaultLanguage]; + $oAction = MetaModel::NewObject(ActionNewsroom::class); + $oAction->Set('name', $aData['name']); + $oAction->Set('status', 'enabled'); + $oAction->Set('language', $sDefaultLanguage); + $oAction->Set('priority', 4); // Standard priority + $oAction->Set('recipients', 'SELECT Person WHERE id = :this->agent_id'); + $oAction->Set('title', '$this->friendlyname$'); + $oAction->Set('message', $aData['message']); + $oAction->DBWrite(); + + // TriggerOnReachingState + SetupLog::Info("Adding default newsroom action for TriggerOnReachingState (not linked to any trigger yet)..."); + + // Actions data for english and french + $aActionsData = [ + 'EN US' => [ + 'name' => 'Notification to agent when ticket assigned', + 'message' => 'Ticket has been assigned to you', + ], + 'FR FR' => [ + 'name' => 'Notification à l\'agent à l\'assignation du ticket', + 'message' => 'Le ticket vous a été assigné', + ], + ]; + + // Create action in app. default language and link it to the triggers + $aData = $aActionsData[$sDefaultLanguage]; + $oAction = MetaModel::NewObject(ActionNewsroom::class); + $oAction->Set('name', $aData['name']); + $oAction->Set('status', 'enabled'); + $oAction->Set('language', $sDefaultLanguage); + $oAction->Set('priority', 3); // Important priority + $oAction->Set('recipients', 'SELECT Person WHERE id = :this->agent_id'); + $oAction->Set('title', '$this->friendlyname$'); + $oAction->Set('message', $aData['message']); + $oAction->DBWrite(); + } + + //N°824 - Fill object_class in EventNotification from the Triggers target_class + if (version_compare($sPreviousVersion, '3.2.0', '<')) { + SetupLog::Info("Filling object_class in EventNotification from the Triggers target_class"); + $iNbProcessed = 0; + + $sTableToSet = MetaModel::DBGetTable('EventNotification', 'object_class'); + $oAttDefToSet = MetaModel::GetAttributeDef('EventNotification', 'object_class'); + $oAttDefObjectId = MetaModel::GetAttributeDef('EventNotification', 'object_id'); + $oAttDefTriggerId = MetaModel::GetAttributeDef('EventNotification', 'trigger_id'); + + $aColumnsToSets = array_keys($oAttDefToSet->GetSQLColumns()); + $sColumnToSet = $aColumnsToSets[0]; // We know that a string has only one column + $aColumnsTriggerId = array_keys($oAttDefTriggerId->GetSQLColumns()); + $sColumnTriggerId = $aColumnsTriggerId[0]; // We know that a string has only one column + $aColumnsObjectd = array_keys($oAttDefObjectId->GetSQLColumns()); + $sColumnObjectId = $aColumnsObjectd[0]; // We know that a string has only one column + + $oSearch = DBObjectSearch::FromOQL('SELECT TriggerOnObject'); + $oSet = new DBObjectSet($oSearch); + $aTriggerIdToTargetClass = []; + while ($oTrigger = $oSet->Fetch()) { + $aTriggerIdToTargetClass[$oTrigger->GetKey()] = $oTrigger->Get('target_class'); + } + + foreach ($aTriggerIdToTargetClass as $sKey => $sTargetClass) { + + if (MetaModel::HasChildrenClasses($sTargetClass)) { + //in this case, we have toget the name of the final class + $sTableToRead = MetaModel::DBGetTable($sTargetClass, 'finalclass'); + $oAttDefToRead = MetaModel::GetAttributeDef($sTargetClass, 'finalclass'); + $aColumnsToReads = array_keys($oAttDefToRead->GetSQLColumns()); + $sColumnToRead = $aColumnsToReads[0]; // We know that a string has only one column + $sObjectPrimaryKey = MetaModel::DBGetKey($sTargetClass); + + $sRepair = "UPDATE `$sTableToSet` JOIN `$sTableToRead` ON `$sTableToSet`.`$sColumnObjectId` = `$sTableToRead`.`$sObjectPrimaryKey` SET `$sTableToSet`.`$sColumnToSet` = `$sTableToRead`.`$sColumnToRead` WHERE `$sTableToSet`.`$sColumnTriggerId` = '".$sKey."' AND `$sTableToSet`.`$sColumnToSet` = ''"; + } else { + + $sRepair = "UPDATE `$sTableToSet` SET `$sTableToSet`.`$sColumnToSet` = '".$sTargetClass."' WHERE `$sTableToSet`.`$sColumnTriggerId` = '".$sKey."' AND `$sTableToSet`.`$sColumnToSet` = ''"; + } + + SetupLog::Info(" | | Query: ".$sRepair); + CMDBSource::Query($sRepair); + $iNbProcessed += CMDBSource::AffectedRows(); + } + SetupLog::Info("| | ".$iNbProcessed." EventNotification processed."); + } + } + } +} diff --git a/tests/php-unit-tests/unitary-tests/setup/ressources/installation_330.xml b/tests/php-unit-tests/unitary-tests/setup/ressources/installation_330.xml new file mode 100644 index 0000000000..82f159e521 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/setup/ressources/installation_330.xml @@ -0,0 +1,243 @@ + + + + + Configuration Management options + The options below allow you to configure the type of elements that are to be managed inside iTop.]]> + /images/icons/icons8-apps-tab.svg + + + itop-config-mgmt-core + Configuration Management Core + All the base objects that are mandatory in the iTop CMDB: Organizations, Locations, Teams, Persons, etc. + + itop-structure + itop-config-mgmt + itop-attachments + itop-profiles-itil + itop-welcome-itil + itop-tickets + itop-files-information + combodo-db-tools + itop-core-update + itop-hub-connector + itop-oauth-client + combodo-backoffice-darkmoon-theme + combodo-backoffice-fullmoon-high-contrast-theme + combodo-backoffice-fullmoon-protanopia-deuteranopia-theme + combodo-backoffice-fullmoon-tritanopia-theme + itop-themes-compat + combodo-my-account + combodo-my-account-user-info + combodo-oauth2-client + itop-attribute-class-set + itop-attribute-encrypted-password + itop-ui-copypaste + + true + + + itop-config-mgmt-datacenter + Data Center Devices + Manage Data Center devices such as Racks, Enclosures, PDUs, etc. + + itop-datacenter-mgmt + + true + + + itop-config-mgmt-end-user + End-User Devices + Manage devices related to end-users: PCs, Phones, Tablets, etc. + + itop-endusers-devices + + true + + + itop-config-mgmt-storage + Storage Devices + Manage storage devices such as NAS, SAN Switches, Tape Libraries and Tapes, etc. + + itop-storage-mgmt + + true + + + itop-config-mgmt-virtualization + Virtualization + Manage Hypervisors, Virtual Machines and Farms. + + itop-virtualization-mgmt + + true + + + + itop-container-mgmt + Containerization + + + itop-container-mgmt + + false + + + + + + + + Service Management options + Select the choice that best describes the relationships between the services and the IT infrastructure in your IT environment.]]> + /images/icons/icons8-services.svg + + + itop-service-mgmt-enterprise + Service Management for Enterprises + Select this option if the IT delivers services based on a shared infrastructure. For example if different organizations within your company subscribe to services (like Mail and Print services) delivered by a single shared backend. + + itop-service-mgmt + + true + + + itop-service-mgmt-service-provider + Service Management for Service Providers + Select this option if the IT manages the infrastructure of independent customers. This is the most flexible model, since the services can be delivered with a mix of shared and customer specific infrastructure devices. + + itop-service-mgmt-provider + + + + + + Tickets Management options + Select the type of tickets you want to use in order to respond to user requests and incidents.]]> + /images/icons/icons8-discussion-forum.svg + + + itop-ticket-mgmt-simple-ticket + Simple Ticket Management + Select this option to use one single type of tickets for all kind of requests. + + itop-request-mgmt + + true + + + + itop-ticket-mgmt-simple-ticket-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-itil + ITIL Compliant Tickets Management + Select this option to have different types of ticket for managing user requests and incidents. Each type of ticket has a specific life cycle and specific fields + + + + itop-ticket-mgmt-itil-user-request + User Request Management + Manage User Request tickets in iTop + + itop-request-mgmt-itil + + + + itop-ticket-mgmt-itil-incident + Incident Management + Manage Incidents tickets in iTop + + itop-incident-mgmt-itil + + + + itop-ticket-mgmt-itil-enhanced-portal + Customer Portal + + + itop-portal + itop-portal-base + + true + + + + + + itop-ticket-mgmt-none + No Tickets Management + Don't manage incidents or user requests in iTop + + + + + + + Change Management options + Select the type of tickets you want to use in order to manage changes to the IT infrastructure.]]> + /images/icons/icons8-change.svg + + + itop-change-mgmt-simple + Simple Change Management + Select this option to use one type of ticket for all kind of changes. + + itop-change-mgmt + + true + + + itop-change-mgmt-itil + ITIL Change Management + Select this option to use Normal/Routine/Emergency change tickets. + + itop-change-mgmt-itil + + + + itop-change-mgmt-none + No Change Management + Don't manage changes in iTop + + + + + + + Additional ITIL tickets + Pick from the list below the additional ITIL processes that are to be implemented in iTop.]]> + /images/icons/icons8-important-book.svg + + + + itop-kown-error-mgmt + Known Errors Management and FAQ + Select this option to track "Known Errors" and FAQs in iTop. + + itop-faq-light + itop-knownerror-mgmt + + + + itop-problem-mgmt + Problem Management + Select this option to track "Problems" in iTop. + + itop-problem-mgmt + + + + + +