N°8761 - Assist in cleaning up data prior to uninstalling extensions (#838)

* N°8761 - Assist in cleaning up data prior to uninstalling extensions - handle transaction ID + add deletion plan screen

* N°8761 - poc of deletion plan screen

* code style

* N°8761 - WIP deletion plan execution

* Delete all parent classes objects + cleanup

* 🌐 translation (EN only)

* remove history

* In case of no leaf class to remove, delete also the child classes

* 🎨 refactor & fix typo

* Analysis not stored anymore in DB

* Analysis for removed modules

* 🌐 dico

* Add spinner to setup button "Go to backoffice"

* Fix count after PR review

* Fix after PR review

* Fix Number of elements to remove

* Fix arrays

---------

Co-authored-by: odain <olivier.dain@combodo.com>
This commit is contained in:
Eric Espié
2026-03-12 15:08:03 +01:00
committed by GitHub
parent cfdf850445
commit e91cc63a70
27 changed files with 621 additions and 484 deletions

View File

@@ -5108,8 +5108,8 @@ abstract class DBObject implements iDisplay
protected function GetReferencingObjectsForDeletion($bAllowAllData = false)
{
$aDependentObjects = [];
$aRererencingMe = MetaModel::EnumReferencingClasses(get_class($this));
foreach ($aRererencingMe as $sRemoteClass => $aExtKeys) {
$aReferencingMe = MetaModel::EnumReferencingClasses(get_class($this));
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if external key doesn't require the deletion cascading

View File

@@ -0,0 +1,20 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b862a55cbf5448fb99f0905a4db6529b",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"composer-runtime-api": "^2.0"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@@ -1,157 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemoverExtension" _delta="define">
<properties>
<category>grant_by_profile</category>
<db_table>data_feature_removal_extension</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes>
<attribute id="extension_code"/>
</attributes>
</reconciliation>
<uniqueness_rules/>
</properties>
<fields>
<field id="extension_code" xsi:type="AttributeString">
<sql>extension_code</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="label" xsi:type="AttributeString">
<sql>label</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="version" xsi:type="AttributeString">
<sql>version</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="module_names" xsi:type="AttributeText">
<sql>module_names</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="status" xsi:type="AttributeString">
<sql>status</sql>
<default_value>none</default_value>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
</fields>
<presentation>
<list>
<items>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</items>
</list>
<search>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</search>
<details>
<item id="extension_code">
<rank>10</rank>
</item>
<item id="status">
<rank>20</rank>
</item>
</details>
</presentation>
<methods/>
</class>
<class id="DataFeatureRemoverAuditRule" _delta="define">
<properties>
<category>grant_by_profile</category>
<db_table>data_feature_removal_auditrule</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes>
<attribute id="rule_name"/>
<attribute id="extension_code"/>
<attribute id="class_name"/>
</attributes>
</reconciliation>
<uniqueness_rules/>
</properties>
<fields>
<field id="rule_name" xsi:type="AttributeString">
<sql>rule_name</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="extension_code" xsi:type="AttributeString">
<sql>extension_code</sql>
<is_null_allowed>false</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="class_name" xsi:type="AttributeText">
<sql>class_name</sql>
<default_value>none</default_value>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
</field>
<field id="count" xsi:type="AttributeInteger">
<sql>count</sql>
<default_value>0</default_value>
<tracking_level>all</tracking_level>
</field>
</fields>
<presentation>
<list>
<items>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</items>
</list>
<search>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</search>
<details>
<item id="rule_name">
<rank>10</rank>
</item>
<item id="extension_code">
<rank>20</rank>
</item>
<item id="class_name">
<rank>30</rank>
</item>
</details>
</presentation>
<methods/>
</class>
</classes>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>

View File

@@ -16,26 +16,42 @@ Dict::Add('EN US', 'English', 'English', [
'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' => 'This utilitary allows you to enable or disable features that are installed in your iTop.',
'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' => 'You will need to analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'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:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:RemovalType' => 'Type of element',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Occurence' => 'Occurence',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'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:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Analysis:RemovalType:FINAL_CLASS' => 'Final class',
'DataFeatureRemoval:Table:Empty' => 'No data to remove',
]);
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
]);

View File

@@ -16,25 +16,42 @@ Dict::Add('FR FR', 'French', 'Français', [
'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' => 'This utilitary allows you to enable or disable features that are installed in your iTop.',
'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' => 'You will need to analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'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:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:RemovalType' => 'Type of element',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Occurence' => 'Occurence',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'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:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'DataFeatureRemoval:Table:Analysis:RemovalType:FINAL_CLASS' => 'Final class',
'DataFeatureRemoval:Table:Empty' => 'No data to remove',
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
]);

View File

@@ -11,113 +11,161 @@ 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\AuthentToken\Helper\TokenAuthLog;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Model\DataFeatureRemoverAuditRuleService;
use Combodo\iTop\DataFeatureRemoval\Model\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
use Dict;
use Exception;
use IssueLog;
use MetaModel;
use utils;
class DataFeatureRemovalController extends Controller
{
private array $aSelectedExtensionsForCheck = [];
private array $aCountClassesToCleanup = [];
private array $aAnalysisDataTable = [];
private int $iCount = 0;
public function OperationMain($sErrorMessage = null)
public function OperationMain($sErrorMessage = null): void
{
$aParams = [];
$this->ReadRemovedExtensions();
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bHasData'] = $this->iCount > 0;
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$this->AddFeatureParams($aParams);
$this->AddAnalyzeParams($aParams);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$this->DisplayPage($aParams);
}
public function AddFeatureParams(array &$aParams)
public function AddAnalyzeParams(): void
{
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['sModule'] = DataFeatureRemovalHelper::MODULE_NAME;
}
public function AddAnalyzeParams(array &$aParams)
{
$iTotalCount = 0;
$aData = [];
$aColumns = [];
foreach (DataFeatureRemoverAuditRuleService::GetInstance()->ReadCheckRules() as $oRule) {
$sContent = $oRule->Get('class_name');
$sModuleName = MetaModel::GetModuleName($sContent);
$this->iCount = 0;
foreach ($this->aCountClassesToCleanup as $sClass => $iCount) {
$sModuleName = MetaModel::GetModuleName($sClass);
$aExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetIncludingExtensions($sModuleName);
$sExtensions = implode(' ', $aExtensions);
$sTypeName = $oRule->Get('rule_name');
$sTypeDesc = \Dict::S("DataFeatureRemoval:Table:Analysis:RemovalType:$sTypeName");
$iCount = $oRule->Get('count');
$iTotalCount += $iCount;
$aColumns = ['ClassName', 'RemovalType','FeatureName','Occurence'];
$aData[] = [
<<<HTML
<label>$sContent</label>
HTML,
<<<HTML
<label>$sTypeDesc</label>
HTML,
<<<HTML
<label title="$sModuleName">$sExtensions</label>
HTML,
<<<HTML
<label>$iCount</label>
HTML,
];
$aColumns = ['ClassName','FeatureName','Module','Occurrence'];
$aData[] = [$sClass,$sExtensions,$sModuleName,$iCount];
$this->iCount += $iCount;
}
$aParams['aCheckRules'] = $this->GetTableData('Analysis', $aColumns, $aData);
$aParams['rule_count'] = $iTotalCount;
$this->aAnalysisDataTable = $this->GetTableData('Analysis', $aColumns, $aData);
}
public function OperationAnalyze()
public function OperationAnalyze(): void
{
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
$this->aSelectedExtensionsForCheck = [];
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
$sValue = $aData['enable'] ?? 'off';
if (($sValue) === 'on') {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
$this->ReadRemovedExtensions();
$this->m_sOperation = 'Main';
try {
$this->Analyze();
if (count($this->aSelectedExtensionsForCheck) > 0) {
$this->Analyze();
}
$this->OperationMain();
} catch (Exception $e) {
\IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
IssueLog::Error(__METHOD__, null, ['stack' => $e->getTraceAsString(), 'exception' => $e->getMessage()]);
$this->OperationMain($e->getMessage());
}
}
private function Analyze(): void
{
$sSourceEnv = MetaModel::GetEnvironment();
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvironment->Prepare($sSourceEnv, $this->aSelectedExtensionsForCheck);
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnv);
$oSetupAudit = new SetupAudit($sSourceEnv, DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$aGetRemovedClasses = $oSetupAudit->GetIssues();
IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
$this->aCountClassesToCleanup = $aGetRemovedClasses;
}
public function OperationDeletionPlan(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
$aRows = [];
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
$aRows[] = [
$oDeletionPlanSummaryEntity->sClass,
$oDeletionPlanSummaryEntity->iDeleteCount,
$oDeletionPlanSummaryEntity->iUpdateCount,
$oDeletionPlanSummaryEntity->sIssue ?? '',
];
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$this->DisplayPage($aParams);
}
public function OperationDoDeletion(): void
{
$aParams = [];
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
$aRows = [];
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
$aRows[] = [
$oDeletionExecutionSummaryEntity->sClass,
$oDeletionExecutionSummaryEntity->iDeleteCount,
$oDeletionExecutionSummaryEntity->iUpdateCount,
];
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$this->DisplayPage($aParams);
}
/**
* Get installed extensions from disk
*
* @return array structure for twig datatable
*/
private function GetExtensionsTable(): array
{
$aExtensions = [];
$aColumns = ['', 'Version', 'Name', 'Code'];
$this->aSelectedExtensionsForCheck = DataFeatureRemoverExtensionService::GetInstance()->ReadAuditedExtensions();
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
/** @var \iTopExtension $oExtension */
$sChecked = "checked";
$sChecked = '';
$sDisabledHtml = '';
if ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
} elseif (! array_key_exists($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = "";
$sChecked = 'checked';
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = 'checked';
}
$sLabel = $oExtension->sLabel;
@@ -128,35 +176,28 @@ HTML,
<<<HTML
<input type="checkbox" $sDisabledHtml class="extension_check" $sChecked id="$sIdEnable" name="$sIdEnable"/>
HTML,
<<<HTML
<label>$sVersion</label>
HTML,
<<<HTML
<label for="$sIdEnable">$sLabel</label>
HTML,
<<<HTML
<label for="$sIdEnable">$sCode</label>
HTML,
$sVersion,
$sLabel,
$sCode,
];
}
return $this->GetTableData('Extensions', $aColumns, $aExtensions);
}
public function GetTableData(string $sTableName, array $aColumns, array $aData): array
private function GetTableData(string $sTableName, array $aColumns, array $aData): array
{
if (empty($aData)) {
return [
'Type' => 'Table',
'Columns' => [['label' => '']],
'Data' => [[ Dict::S('DbCleaner:Table:Empty')]],
'Data' => [[ Dict::S('DataFeatureRemoval:Table:Empty')]],
];
}
$aNewColumns = [];
foreach ($aColumns as $sColumn) {
$aNewColumns[] = ['label' => Dict::S("DataFeatureRemoval:Table:$sTableName:$sColumn", $sColumn)];
$aNewColumns[] = ['label' => Dict::S("DataFeatureRemoval:Table:$sTableName:$sColumn", Dict::S("DataFeatureRemoval:Column:$sColumn", $sColumn))];
}
$aColumns = $aNewColumns;
@@ -167,23 +208,45 @@ HTML,
];
}
private function Analyze()
/**
* @return void
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
private function ValidateTransactionId(): void
{
DataFeatureRemoverExtensionService::GetInstance()->SaveExtensions($this->aSelectedExtensionsForCheck);
if (empty($_POST)) {
return;
}
$sSourceEnvt = \MetaModel::GetEnvironment();
$oDryRemovalRuntimeEnvironment = new DryRemovalRuntimeEnvironment();
$oDryRemovalRuntimeEnvironment->Prepare($sSourceEnvt, $this->aSelectedExtensionsForCheck);
$oDryRemovalRuntimeEnvironment->CompileFrom($sSourceEnvt);
$oSetupAudit = new SetupAudit($sSourceEnvt, DryRemovalRuntimeEnvironment::DRY_REMOVAL_AUDIT_ENV);
$this->Save($oSetupAudit->GetIssues());
$sTransactionId = utils::ReadPostedParam('transaction_id', null, utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID);
IssueLog::Debug(__FUNCTION__.": Transaction [$sTransactionId]");
if (empty($sTransactionId) || !utils::IsTransactionValid($sTransactionId, false)) {
throw new DataFeatureRemovalException(Dict::S("iTopUpdate:Error:InvalidToken"));
}
}
private function Save(array $aGetRemovedClasses)
/**
* @return void
*/
public function ReadRemovedExtensions(): void
{
\IssueLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
if (count($this->aSelectedExtensionsForCheck) > 0) {
return;
}
DataFeatureRemoverAuditRuleService::GetInstance()->SaveChecks($aGetRemovedClasses);
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
foreach ($aSelectedExtensionsFromUI as $sCode => $aData) {
$sValue = $aData['enable'] ?? 'off';
if (($sValue) === 'on') {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
// Add source removed to check
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
if ($oExtension->bRemovedFromDisk) {
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanSummaryEntity
{
public string $sClass;
/**
* @var int : DEL_MANUAL|DEL_AUTO|DEL_SILENT|DEL_MOVEUP|DEL_NONE
* @see \AttributeDefinition DEL_xxx
*/
public int $iMode = 0;
public ?string $sIssue = null;
public int $iUpdateCount = 0;
public int $iDeleteCount = 0;
/**
* @param string $sClass
*/
public function __construct(string $sClass)
{
$this->sClass = $sClass;
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Model;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DataFeatureRemoverAuditRule;
use DBObjectSearch;
use DBObjectSet;
use Exception;
class DataFeatureRemoverAuditRuleService
{
private static DataFeatureRemoverAuditRuleService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverAuditRuleService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverAuditRuleService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverAuditRuleService $oInstance): void
{
self::$oInstance = $oInstance;
}
public function SaveChecks(array $aGetRemovedClasses)
{
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverAuditRule', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$oObj->DBDelete();
}
foreach ($aGetRemovedClasses as $sClass => $iCount) {
$oObj = new DataFeatureRemoverAuditRule();
$oObj->Set('rule_name', 'FINAL_CLASS');
$oObj->Set('extension_code', $sClass);
$oObj->Set('class_name', $sClass);
$oObj->Set('count', $iCount);
$oObj->DBWrite();
}
}
public function ReadCheckRules(): array
{
try {
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverAuditRule', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
$aRes = [];
while (null != ($oObj = $oSet->Fetch())) {
$aRes[] = $oObj;
}
return $aRes;
} catch (Exception $e) {
throw new DataFeatureRemovalException(__FUNCTION__.' failed', 0, $e);
}
}
}

View File

@@ -1,122 +0,0 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Model;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DataFeatureRemoverExtension;
use DBObjectSearch;
use DBObjectSet;
use Exception;
use iTopExtension;
use iTopExtensionsMap;
use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverExtensionService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverExtensionService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverExtensionService $oInstance): void
{
self::$oInstance = $oInstance;
}
public function SaveExtensions(array $aSelectedExtensionsForCheck)
{
$this->ReadItopExtensions();
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverExtension', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$oObj->DBDelete();
}
foreach ($aSelectedExtensionsForCheck as $i => $sCode) {
$oObj = new DataFeatureRemoverExtension();
$oObj->Set('extension_code', $sCode);
/** @var iTopExtension $oExtension */
$oExtension = $this->aItopExtensions[$sCode];
$oObj->Set('module_names', json_encode($oExtension->aModules));
$oObj->Set('label', $oExtension->sLabel);
$oObj->Set('version', $oExtension->sVersion);
$oObj->DBWrite();
}
}
private array $aSelectedExtensions = [];
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
public function ReadAuditedExtensions(): array
{
if (count($this->aSelectedExtensions) == 0) {
try {
$oSearch = DBObjectSearch::FromOQL('SELECT DataFeatureRemoverExtension', []);
$oSearch->AllowAllData();
$oSet = new DBObjectSet($oSearch);
while (null != ($oObj = $oSet->Fetch())) {
$sCode = $oObj->Get('extension_code');
$sLabel = $oObj->Get('label');
$sVersion = $oObj->Get('version');
$sModuleNames = $oObj->Get('module_names');
$aModuleNames = json_decode($sModuleNames, true);
if (is_array($aModuleNames) && count($aModuleNames) > 0) {
foreach ($aModuleNames as $sModuleName) {
$aExtensions = $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
$aExtensions[] = "$sLabel / $sVersion";
$this->aIncludingExtensionsByModuleName[$sModuleName] = $aExtensions;
}
}
$this->aSelectedExtensions[$sCode] = $oObj;
}
} catch (Exception $e) {
throw new DataFeatureRemovalException(__FUNCTION__.' failed', 0, $e);
}
}
\IssueLog::Debug(__METHOD__, null, ['aSelectedExtensionsForCheck' => $this->aSelectedExtensions]);
\IssueLog::Debug(__METHOD__, null, ['aIncludingExtensionsByModuleName' => $this->aIncludingExtensionsByModuleName]);
return $this->aSelectedExtensions;
}
public function GetIncludingExtensions(string $sModuleName): array
{
$this->ReadAuditedExtensions();
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
}
/**
* @return iTopExtension[]
*/
public function ReadItopExtensions(): array
{
if (count($this->aItopExtensions) == 0) {
$oExtensionsMap = new iTopExtensionsMap();
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);
});
}
return $this->aItopExtensions;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use iTopExtension;
use iTopExtensionsMap;
use MetaModel;
class DataFeatureRemoverExtensionService
{
private static DataFeatureRemoverExtensionService $oInstance;
private array $aItopExtensions = [];
private array $aIncludingExtensionsByModuleName = [];
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemoverExtensionService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemoverExtensionService();
}
return self::$oInstance;
}
final public static function SetInstance(?DataFeatureRemoverExtensionService $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* @param string $sModuleName
*
* @return array
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function GetIncludingExtensions(string $sModuleName): array
{
if (count($this->aIncludingExtensionsByModuleName) === 0) {
foreach ($this->ReadItopExtensions() as $oExtension) {
$aModuleNames = $oExtension->aModules;
if (is_array($aModuleNames) && count($aModuleNames) > 0) {
foreach ($aModuleNames as $sModule) {
$aExtensions = $this->aIncludingExtensionsByModuleName[$sModule] ?? [];
$aExtensions[] = $oExtension->sLabel.'/'.$oExtension->sVersion;
$this->aIncludingExtensionsByModuleName[$sModule] = $aExtensions;
}
}
}
}
return $this->aIncludingExtensionsByModuleName[$sModuleName] ?? [];
}
/**
* @return iTopExtension[]
*/
public function ReadItopExtensions(): array
{
if (count($this->aItopExtensions) === 0) {
$oExtensionsMap = new iTopExtensionsMap();
$oExtensionsMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
$this->aItopExtensions = $oExtensionsMap->GetAllExtensionsToDisplayInSetup(true);
uasort($this->aItopExtensions, function (iTopExtension $oiTopExtension1, iTopExtension $oiTopExtension2) {
return strcmp($oiTopExtension1->sLabel, $oiTopExtension2->sLabel);
});
}
return $this->aItopExtensions;
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
class DeletionPlanService
{
private static DeletionPlanService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DeletionPlanService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new DeletionPlanService();
}
return self::$oInstance;
}
final public static function SetInstance(?DeletionPlanService $oInstance): void
{
self::$oInstance = $oInstance;
}
/**
* Get a summary of the deletion plan computed for the classes.
* The result is used for display
*
* @param array|null $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlanSummary(?array $aClasses): array
{
$aSummary = [];
if (is_null($aClasses)) {
return $aSummary;
}
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aUpdates) {
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iUpdateCount = count($aUpdates);
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iDeleteCount = count($aDeletes);
$aDelete = array_shift($aDeletes);
$oDeletionPlanSummaryEntity->iMode = $aDelete['mode'];
$oDeletionPlanSummaryEntity->sIssue = $aDelete['issue'] ?? null;
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* @param string $sClass
*
* @return \DBObject[]
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Exception
*/
private function GetAllObjects(string $sClass): array
{
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
return $oSet->ToArray();
}
/**
* @param array $aClasses
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses): array
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
if (count($oDeletionPlan->GetIssues()) > 0) {
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
}
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aToUpdate as $aData) {
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
$oToUpdate->Set($sRemoteExtKey, $aData['values'][$sRemoteExtKey]);
}
$oToUpdate->DBUpdate();
$oDeletionPlanSummaryEntity->iUpdateCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
foreach ($oDeletionPlan->ListDeletes() as $sClass => $aDeletes) {
$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);
// 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);
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
}
return $aSummary;
}
/**
* Get a deletion plan for all the objects of the classes
*
* @param array $aClasses array of class names to clean
*
* @return \DeletionPlan
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function GetDeletionPlan(array $aClasses): DeletionPlan
{
$oDeletionPlan = new DeletionPlan();
foreach ($aClasses as $sClass) {
$aObjects = $this->GetAllObjects($sClass);
foreach ($aObjects as $oObject) {
$oObject->CheckToDelete($oDeletionPlan);
}
}
return $oDeletionPlan;
}
}

View File

@@ -0,0 +1,25 @@
{# @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 } %}
{% 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 %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -0,0 +1,14 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DoDeletion:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DoDeletion:SubTitle'|dict_s } %}
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back_to_main', sId:'btn_back_to_main', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -0,0 +1,19 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% if bHasData %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:SubTitle'|dict_format(iCount) } %}
{% UIDataTable ForForm { sRef:'aAnalysisDataTable', aColumns:aAnalysisDataTable.Columns, aData:aAnalysisDataTable.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DeletionPlan'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:PlanDeletion'|dict_s, sName:'btn_plandeletion', sId:'btn_plandeletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% endif %}

View File

@@ -1,8 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIForm Standard {} %}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:SubTitle'|dict_format(rule_count) } %}
{% UIDataTable ForForm { sRef:'aCheckRules', aColumns:aCheckRules.Columns, aData:aCheckRules.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% EndUIForm %}

View File

@@ -10,9 +10,7 @@
{% UIDataTable ForForm { sRef:'aExtensions', aColumns:aExtensions.Columns, aData:aExtensions.Data} %}{% EndUIDataTable %}
{% EndUIFieldSet %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Analyze'|dict_s, sName:'btn_apply', sId:'btn_apply', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}

View File

@@ -8,21 +8,28 @@
{# DataFeatureRemoval #}
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Main:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Main:SubTitle'|dict_s } %}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Main:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Main:SubTitle'|dict_s } %}
{% UIAlert ForInformation { sTitle:'DataFeatureRemoval:Helper:Title'|dict_s } %}
{{ 'DataFeatureRemoval:Helper:Desc1'|dict_s }}<BR>
{{ 'DataFeatureRemoval:Helper:Desc2'|dict_s }}
{% EndUIAlert %}
{% if null != DataFeatureRemovalErrorMessage %}
<div id="feature_removal_error_msg_div" style="display:block">
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
{% EndUIAlert %}
</div>
{% endif %}
{% if null != DataFeatureRemovalErrorMessage %}
<div id="feature_removal_error_msg_div" style="display:block">
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
{% EndUIAlert %}
</div>
{% endif %}
{% include 'Features.html.twig' %}
{% include 'ExtensionRemovalData.html.twig' %}
{% include 'FeaturesTab.html.twig' %}
{% include 'ExtensionRemovalDataTab.html.twig' %}
{% if not bHasData %}
{% UIToolbar ForButton {} %}
<a href="{{ sSetupUrl }}">
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:false} %}
</a>
{% EndUIToolbar %}
{% endif %}
{% EndUIPanel %}

View File

@@ -1,9 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
$(document).on('click', '#checkAllExtensions', function() {
var bChecked = this.checked;
$('.extension_check').each( function() { this.checked = bChecked });
});

View File

@@ -1,2 +0,0 @@
{# @copyright Copyright (C) 2010-2024 Combodo SAS #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}

View File

@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -7,11 +7,11 @@ $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\\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',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverAuditRuleService' => $baseDir . '/src/Model/DataFeatureRemoverAuditRuleService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Model/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\SetupAudit' => $baseDir . '/src/Service/SetupAudit.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -26,12 +26,12 @@ 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\\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',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverAuditRuleService' => __DIR__ . '/../..' . '/src/Model/DataFeatureRemoverAuditRuleService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Model\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Model/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\SetupAudit' => __DIR__ . '/../..' . '/src/Service/SetupAudit.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);

View File

@@ -16,7 +16,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\WebPage\WebPage;
require_once(APPROOT.'setup/setuputils.class.inc.php');
require_once(APPROOT.'setup/parameters.class.inc.php');
@@ -243,6 +242,7 @@ HTML;
}
$oPage->add('</tr></table>');
$oPage->add("</form>");
$oStep->PostFormDisplay($oPage);
$oPage->add('<div id="async_action" style="display:none;overflow:auto;max-height:100px;color:#F00;font-size:small;"></div>'); // The div may become visible in case of error
// Hack to have the "Next >>" button, be the default button, since the first submit button in the form is the default one

View File

@@ -17,6 +17,7 @@
*
* You should have received a copy of the GNU Affero General Public License
*/
use Combodo\iTop\Application\WebPage\WebPage;
require_once(APPROOT.'setup/sequencers/DataAuditSequencer.php');
@@ -51,7 +52,6 @@ class WizStepDataAudit extends WizStepInstall
} else {
return false;
}
}
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
@@ -76,8 +76,6 @@ class WizStepDataAudit extends WizStepInstall
$sAuthentToken = $this->oWizard->GetParameter('authent', '');
$oPage->add('<input type="hidden" id="authent_token" value="'.$sAuthentToken.'"/>');
$sApplicationUrl = $this->oWizard->GetParameter('application_url').'pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php';
$oPage->add('<input type="hidden" id="application_url" value="'.$sApplicationUrl.'"/>');
if (!$this->CheckDependencies()) {
$oPage->error($this->sDependencyIssue);
$oPage->add_ready_script(<<<JS
@@ -90,7 +88,39 @@ JS);
ExecuteStep("");
JS);
}
}
/**
* Add extra form to go to the feature removal backend page
* @param \SetupPage $oPage
*
* @return void
*/
public function PostFormDisplay(SetupPage $oPage)
{
$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 .= <<<INPUT
<input type="hidden" name="aExtensions[$sSafeExtCode][enable]" value="on"/>
INPUT;
}
$oPage->add(
<<<HTML
<form id="data-feature-removal" class="ibo-setup--wizard ibo-is-hidden" method="post" action="$sApplicationUrl">
<input type="hidden" name="operation" value="Analyze"/>
$aHiddenRemovedExtensionInputs
</form>
HTML
);
}
protected function AddProgressErrorScript($oPage, $aRes)
@@ -101,8 +131,13 @@ JS);
<<<EOF
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').before('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="next"><span class="ibo-button--label">Ignore and continue</span></button></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><a href="'+$('#application_url').val()+'"><button class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Go to backoffice</span></button></a></td>');
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><span id="submit-wait" class="ibo-spinner ibo-is-inline ibo-is-hidden ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"></i></span>&nbsp;<button id="goto-data-feature-removal" class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Go to backoffice</span></button></td>');
$('#goto-data-feature-removal').on("click", function() {
$('#goto-data-feature-removal').prop('disabled', true);
$('#submit-wait').removeClass("ibo-is-hidden");
$('#data-feature-removal').submit();
})
$("#wiz_form").data("installation_status", "cleanup_needed");
$('#btn_next').hide();
EOF

View File

@@ -66,6 +66,16 @@ abstract class WizardStep
protected $bDependencyCheck = null;
protected $sDependencyIssue = null;
/**
* Add post display stuff to the setup screen
* @param \SetupPage $oPage
*
* @return void
*/
public function PostFormDisplay(SetupPage $oPage)
{
}
protected function CheckDependencies()
{
if (is_null($this->bDependencyCheck)) {