From 37ecd8f63f289db75a1aa193cbd2af71d0036ec4 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Mon, 16 Mar 2026 13:54:07 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B08761=20-=20Assist=20in=20cleaning=20up?= =?UTF-8?q?=20data=20prior=20to=20uninstalling=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/log.class.inc.php | 22 ++++++ ...datamodel.combodo-data-feature-removal.xml | 5 ++ .../en.dict.combodo-data-feature-removal.php | 3 +- .../fr.dict.combodo-data-feature-removal.php | 69 +++++++++--------- .../DataFeatureRemovalController.php | 6 ++ .../src/Helper/DataFeatureRemovalConfig.php | 71 +++++++++++++++++++ .../src/Service/DeletionPlanService.php | 32 +++++---- .../templates/DeletionPlan.html.twig | 26 ++++--- .../vendor/composer/autoload_classmap.php | 1 + .../vendor/composer/autoload_static.php | 1 + 10 files changed, 178 insertions(+), 58 deletions(-) create mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php diff --git a/core/log.class.inc.php b/core/log.class.inc.php index b79703ebf..0e4962c43 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -691,6 +691,28 @@ abstract class LogAPI static::$m_oMockMetaModelConfig = $oMetaModelConfig; } + public static function Exception(string $sMessage, throwable $oException, string $sChannel = null, array $aContext = []): void + { + $aErrorLogs = []; + $aErrorLogs[] = static::PrepareErrorLog($sMessage, $oException, $aContext); + $oException = $oException->getPrevious(); + while ($oException !== null) { + $aErrorLogs[] = static::PrepareErrorLog($oException->getMessage(), $oException, $aContext, true); + $oException = $oException->getPrevious(); + } + $aErrorLogs = array_reverse($aErrorLogs); + foreach ($aErrorLogs as $aErrorLog) { + static::Error($aErrorLog['message'], $sChannel, $aErrorLog['context']); + } + } + + private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array + { + $aContext['Error Message'] = $oException->getMessage(); + $aContext['Stack Trace'] = $oException->getTraceAsString(); + return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext]; + } + public static function Error($sMessage, $sChannel = null, $aContext = []) { static::Log(self::LEVEL_ERROR, $sMessage, $sChannel, $aContext); diff --git a/datamodels/2.x/combodo-data-feature-removal/datamodel.combodo-data-feature-removal.xml b/datamodels/2.x/combodo-data-feature-removal/datamodel.combodo-data-feature-removal.xml index f85d4d010..669a2fe27 100644 --- a/datamodels/2.x/combodo-data-feature-removal/datamodel.combodo-data-feature-removal.xml +++ b/datamodels/2.x/combodo-data-feature-removal/datamodel.combodo-data-feature-removal.xml @@ -8,4 +8,9 @@ 1 + + + 100 + + 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 5a87e5a6a..f91965690 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 @@ -25,9 +25,10 @@ Dict::Add('EN US', 'English', 'English', [ 'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing', 'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan', - 'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing', + 'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s rows to clean before continuing', 'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion', 'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database', + 'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Too many entries to clean', 'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove', 'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name', 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 ffe23414b..d26e701ce 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 @@ -10,48 +10,49 @@ */ Dict::Add('FR FR', 'French', 'Français', [ - 'Menu:DataFeatureRemovalMenu' => 'Features Removal', - 'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal', + 'Menu:DataFeatureRemovalMenu' => 'Suppression de fonctionnalités', + 'combodo-data-feature-removal/Operation:Main/Title' => 'Suppression de fonctionnalités', - 'DataFeatureRemoval:Main:Title' => 'Features Removal', - 'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup', - 'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors', - 'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.', - 'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.', - 'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.', + 'DataFeatureRemoval:Main:Title' => 'Suppression de fonctionnalités', + 'DataFeatureRemoval:Main:SubTitle' => 'Préparez les fonctionnalités que vous souhaitez activer ou désactiver lors d’une prochaine configuration', + 'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression de fonctionnalités', + 'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les fonctionnalités installées dans votre iTop.', + 'DataFeatureRemoval:Helper:Desc1' => 'Cette étape prépare l’assistant de configuration à activer ou désactiver des fonctionnalités.', + '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' => 'Features', - 'DataFeatureRemoval:Analysis:Title' => 'Analysis result', - 'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing', + 'DataFeatureRemoval:Features:Title' => 'Fonctionnalités', + 'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse', + 'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre', - 'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan', - 'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing', - 'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion', - 'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database', + 'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression', + 'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s ligne(s) à nettoyer avant de poursuivre', + 'DataFeatureRemoval:DoDeletion:Title' => 'Exécuter la suppression', + 'DataFeatureRemoval:DoDeletion:SubTitle' => 'Supprime toutes les entrées de la base de données', + 'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Trop d’entrées à nettoyer', - 'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove', - 'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name', - 'DataFeatureRemoval:Table:Analysis:Module' => 'Module name', + 'DataFeatureRemoval:Table:Analysis:ClassName' => 'Élément à supprimer', + 'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Fonctionnalité', + 'DataFeatureRemoval:Table:Analysis:Module' => 'Module', 'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence', - 'UI:Button:Analyze' => 'Analyze', - 'UI:Button:ModifyChoices' => 'Modify Choices', - 'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup', - 'UI:Button:PlanDeletion' => 'Prepare deletion plan', - 'UI:Button:DoDeletion' => 'Delete data', - 'UI:Button:BackToMain' => 'Back to Feature Removal', - 'UI:Button:Setup' => 'Back to setup', + 'UI:Button:Analyze' => 'Analyser', + 'UI:Button:ModifyChoices' => 'Modifier les choix', + 'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration', + 'UI:Button:PlanDeletion' => 'Préparer le plan de suppression', + 'UI:Button:DoDeletion' => 'Supprimer les données', + 'UI:Button:BackToMain' => 'Retour à la suppression de fonctionnalités', + 'UI:Button:Setup' => 'Retour à l’assistant de configuration', - 'UI:Action:ForceUninstall' => 'Force uninstall', - 'UI:Action:MoreInfo' => 'More information', + 'UI:Action:ForceUninstall' => 'Forcer la désinstallation', + 'UI:Action:MoreInfo' => 'Plus d’informations', - 'DataFeatureRemoval:Table:Empty' => 'No data to remove', + 'DataFeatureRemoval:Table:Empty' => 'Aucune donnée à supprimer', - 'DataFeatureRemoval:Column:Class' => 'Class', - 'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete', - 'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update', - 'DataFeatureRemoval:Column:Issue' => 'Issue', + 'DataFeatureRemoval:Column:Class' => 'Classe', + 'DataFeatureRemoval:Column:DeleteCount' => 'Entrées à supprimer', + 'DataFeatureRemoval:Column:UpdateCount' => 'Entrées à mettre à jour', + 'DataFeatureRemoval:Column:Issue' => 'Problème', - 'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries', - 'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries', + 'DataFeatureRemoval:Column:DeletedCount' => 'Entrées supprimées', + 'DataFeatureRemoval:Column:UpdatedCount' => 'Entrées mises à jour', ]); 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 6ad0ed8e1..d92ca9077 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 @@ -11,6 +11,7 @@ require_once APPROOT.'setup/feature_removal/SetupAudit.php'; require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php'; use Combodo\iTop\Application\TwigBase\Controller\Controller; +use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig; use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException; use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper; use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService; @@ -107,6 +108,7 @@ class DataFeatureRemovalController extends Controller $aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses); $aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue']; $aRows = []; + $iQueryCount = 0; foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) { $aRows[] = [ $oDeletionPlanSummaryEntity->sClass, @@ -114,11 +116,15 @@ class DataFeatureRemovalController extends Controller $oDeletionPlanSummaryEntity->iUpdateCount, $oDeletionPlanSummaryEntity->sIssue ?? '', ]; + $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'] = ($iQueryCount <= DataFeatureRemovalConfig::GetInstance()->Get('max_count_estimation_for_safe_cleanup', 100)); $this->DisplayPage($aParams); } diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php b/datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php new file mode 100644 index 000000000..cd5ab17f3 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php @@ -0,0 +1,71 @@ +Get($sParamName, $default); + + return boolval($res); + } + + public function IsEnabled(): bool + { + return $this->GetBoolean('enable', false); + } + + public function Set(string $sParamName, $value) + { + $oConfig = utils::GetConfig(); + $oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value); + } + + /** + * @param \Config|null $oConfig + * + * @return void + * @throws \ConfigException + * @throws \CoreException + */ + public function SaveItopConfiguration(Config $oConfig = null) + { + if (is_null($oConfig)) { + $oConfig = utils::GetConfig(); + } + $sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php'; + @chmod($sConfigFile, 0770); // Allow overwriting the file + $oConfig->WriteToFile($sConfigFile); + @chmod($sConfigFile, 0444); // Read-only + } +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php index bce90b8bf..c0cce332b 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php @@ -126,20 +126,28 @@ class DeletionPlanService $oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass); foreach ($aDeletes as $sId => $aDelete) { - // Delete any existing change tracking about the current object - $oFilter = new DBObjectSearch('CMDBChangeOp'); - $oFilter->AddCondition('objclass', $sClass, '='); - $oFilter->AddCondition('objkey', $sId, '='); - MetaModel::PurgeData($oFilter); + try { + CMDBSource::Query('START TRANSACTION'); + // Delete any existing change tracking about the current object + $oFilter = new DBObjectSearch('CMDBChangeOp'); + $oFilter->AddCondition('objclass', $sClass, '='); + $oFilter->AddCondition('objkey', $sId, '='); + MetaModel::PurgeData($oFilter); - // Delete the entry - $aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false)); - foreach ($aClassesToRemove as $sParentClass) { - $oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id"); - $sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]); - CMDBSource::DeleteFrom($sQuery); + // Delete the entry + $aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false)); + foreach ($aClassesToRemove as $sParentClass) { + $oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id"); + $sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]); + CMDBSource::DeleteFrom($sQuery); + } + + CMDBSource::Query('COMMIT'); + } catch (\Exception $e) { + \IssueLog::Exception(__METHOD__.': Cleanup failed', $e); + CMDBSource::Query('ROLLBACK'); + throw $e; } - $oDeletionPlanSummaryEntity->iDeleteCount++; } diff --git a/datamodels/2.x/combodo-data-feature-removal/templates/DeletionPlan.html.twig b/datamodels/2.x/combodo-data-feature-removal/templates/DeletionPlan.html.twig index d4edc84fc..138ad5fb6 100644 --- a/datamodels/2.x/combodo-data-feature-removal/templates/DeletionPlan.html.twig +++ b/datamodels/2.x/combodo-data-feature-removal/templates/DeletionPlan.html.twig @@ -1,20 +1,24 @@ {# @copyright Copyright (C) 2010-2026 Combodo SARL #} {# @license http://opensource.org/licenses/AGPL-3.0 #} -{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_s } %} +{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_format(iQueryCount) } %} {% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %} {% EndUIPanel %} -{% 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 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 %} +{% else %} + {{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }} +{% endif %} {% UIForm Standard {} %} {% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %} diff --git a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php index e47194c95..97dffa124 100644 --- a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php @@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir); return array( 'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php', 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => $baseDir . '/src/Entity/DeletionPlanSummaryEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php', diff --git a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_static.php b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_static.php index 8f127af07..584484cfa 100644 --- a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_static.php +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_static.php @@ -27,6 +27,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464 public static $classMap = array ( 'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php', 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanSummaryEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php', 'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',