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',