N°9165 - secure data cleanup

This commit is contained in:
Eric Espié
2026-04-30 10:20:25 +02:00
committed by GitHub
parent 0fc7434794
commit 4582256f01
31 changed files with 1505 additions and 511 deletions

View File

@@ -4,8 +4,7 @@
},
"autoload": {
"psr-4": {
"Combodo\\iTop\\DataFeatureRemoval\\": "src",
"": "src/NoNamespace"
"Combodo\\iTop\\DataFeatureRemoval\\": "src"
}
},
"name": "combodo/combodo-data-feature-removal",

View File

@@ -28,7 +28,7 @@ Dict::Add('EN US', 'English', 'English', [
'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:DeletionPlan:Error:Issues' => 'Some objects must be deleted manually prior to cleanup',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
@@ -51,7 +51,7 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:IssueCount' => 'Issues found preventing automatic cleanup',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',

View File

@@ -28,7 +28,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'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 dentrées à nettoyer',
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Certains objets doivent être supprimés manuellement avant le nettoyage',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Élément à supprimer',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Fonctionnalité',
@@ -51,7 +51,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:Column:Class' => 'Classe',
'DataFeatureRemoval:Column:DeleteCount' => 'Entrées à supprimer',
'DataFeatureRemoval:Column:UpdateCount' => 'Entrées à mettre à jour',
'DataFeatureRemoval:Column:Issue' => 'Problème',
'DataFeatureRemoval:Column:IssueCount' => 'Problèmes empêchant le nettoyage automatique',
'DataFeatureRemoval:Column:DeletedCount' => 'Entrées supprimées',
'DataFeatureRemoval:Column:UpdatedCount' => 'Entrées mises à jour',

View File

@@ -14,8 +14,8 @@ 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\DataCleanupService;
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;
@@ -104,17 +104,20 @@ class DataFeatureRemovalController extends Controller
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
$oDataCleanupService = new DataCleanupService();
$aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'IssueCount'];
$aRows = [];
$iQueryCount = 0;
$bHasIssues = false;
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
$aRows[] = [
$oDeletionPlanSummaryEntity->sClass,
$oDeletionPlanSummaryEntity->iDeleteCount,
$oDeletionPlanSummaryEntity->iUpdateCount,
$oDeletionPlanSummaryEntity->sIssue ?? '',
$oDeletionPlanSummaryEntity->iIssueCount,
];
$bHasIssues |= ($oDeletionPlanSummaryEntity->iIssueCount !== 0);
$iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount;
$iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount;
}
@@ -123,7 +126,7 @@ class DataFeatureRemovalController extends Controller
$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));
$aParams['bDeletionPossible'] = !$bHasIssues;
$this->DisplayPage($aParams);
}
@@ -135,7 +138,8 @@ class DataFeatureRemovalController extends Controller
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$oDataCleanupService = new DataCleanupService();
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
$aRows = [];
foreach ($aDeletionExecutionSummary as $oDeletionExecutionSummaryEntity) {
@@ -148,6 +152,7 @@ class DataFeatureRemovalController extends Controller
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionExecutionSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$this->DisplayPage($aParams);
}

View File

@@ -2,16 +2,10 @@
namespace Combodo\iTop\DataFeatureRemoval\Entity;
class DeletionPlanSummaryEntity
class DataCleanupSummaryEntity
{
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 $iIssueCount = 0;
public int $iUpdateCount = 0;
public int $iDeleteCount = 0;

View File

@@ -20,11 +20,11 @@ class DataFeatureRemovalConfig
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(static::$oInstance)) {
static::$oInstance = new DataFeatureRemovalConfig();
if (!isset(self::$oInstance)) {
self::$oInstance = new DataFeatureRemovalConfig();
}
return static::$oInstance;
return self::$oInstance;
}
public function Get(string $sParamName, $default = null)

View File

@@ -0,0 +1,177 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBObjectSet;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use Combodo\iTop\Service\Limits\ExecutionLimits;
use DBObject;
use DBObjectSearch;
use MetaModel;
class DataCleanupService
{
private array $aVisited = [];
private iObjectService $oObjectService;
private ExecutionLimits $oExecutionLimits;
public function __construct(int $iMaxExecutionTime = 30, int $iMaxMemoryPercent = 80)
{
DataFeatureRemovalLog::Enable();
$this->oExecutionLimits = new ExecutionLimits($iMaxExecutionTime, $iMaxMemoryPercent);
}
/**
* 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\DataCleanupSummaryEntity>
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
*/
public function GetCleanupSummary(?array $aClasses): array
{
return $this->ExecuteCleanup($aClasses ?? [], oObjectService: new ObjectServiceSummary());
}
private function GetNextObjectToDelete(array $aClasses): ?DBObject
{
foreach ($aClasses as $sClass) {
$oFilter = new DBObjectSearch($sClass);
$oFilter->AllowAllData();
$oSet = new \DBObjectSet($oFilter);
while ($oObject = $oSet->Fetch()) {
if (!$this->IsVisited($oObject)) {
return $oObject;
}
}
}
return null;
}
/**
* @param array $aClasses
* @param \Combodo\iTop\DataFeatureRemoval\Service\iObjectService|null $oObjectService
*
* @return array execution summary
* @throws \ArchivedObjectException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteCleanup(array $aClasses, ?iObjectService $oObjectService = null): array
{
$this->oObjectService = $oObjectService ?? new ObjectService();
$this->aVisited = [];
while ($oObject = $this->GetNextObjectToDelete($aClasses)) {
if ($this->RecursiveDeletion($oObject) === false) {
// Timeout, stop here
break;
}
}
return $this->oObjectService->GetSummary();
}
private function MarkObjectAsVisited(DBObject $oObject): void
{
$sClass = get_class($oObject);
$sId = $oObject->GetKey();
$sKey = "$sClass-$sId";
$this->aVisited[$sKey] = true;
}
private function IsVisited(DBObject $oObject): bool
{
$sClass = get_class($oObject);
$sId = $oObject->GetKey();
$sKey = "$sClass-$sId";
$bRes = $this->aVisited[$sKey] ?? false;
DataFeatureRemovalLog::Debug('Checking if object is visited', null, [$sKey, $bRes]);
return $bRes;
}
/**
*
* @param \DBObject $oObjectToClean
*
* @return bool true if deletion is complete, false in case of timeout or memory limit reached
*
* @throws \ArchivedObjectException
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
private function RecursiveDeletion(DBObject $oObjectToClean): bool
{
$this->MarkObjectAsVisited($oObjectToClean);
$sClass = get_class($oObjectToClean);
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
/** @var \AttributeExternalKey $oExtKeyAttDef */
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
// skip if this external key is behind an external field
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
continue;
}
$oSearch = new DBObjectSearch($sRemoteClass);
$oSearch->AddCondition($sExtKeyAttCode, $oObjectToClean->GetKey(), '=');
$oSearch->AllowAllData();
$oSet = new CMDBObjectSet($oSearch);
$oSet->OptimizeColumnLoad([$sRemoteClass => [$oExtKeyAttDef->GetCode()]]);
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
// Move the child up one level i.e. set the same parent as the current object
$iParentId = $oObjectToClean->Get($oExtKeyAttDef->GetCode());
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), $iParentId);
} else {
$this->oObjectService->Update($oDependentObj, $oExtKeyAttDef->GetCode(), 0);
}
if ($this->oExecutionLimits->ShouldStopExecution()) {
return false;
}
} else {
// Propagate deletion only if not visited
if ($this->IsVisited($oDependentObj)) {
continue;
}
if (!$this->RecursiveDeletion($oDependentObj)) {
// Timeout
return false;
}
}
}
}
}
$this->oObjectService->Delete($sClass, $oObjectToClean->GetKey());
if ($this->oExecutionLimits->ShouldStopExecution()) {
return false;
}
return true;
}
}

View File

@@ -1,182 +0,0 @@
<?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) {
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);
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
$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,60 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use DBObject;
use DBObjectSearch;
use MetaModel;
class ObjectService extends ObjectServiceSummary
{
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
{
$oToUpdate->Set($sAttCode, $value);
$oToUpdate->DBUpdate();
parent::Update($oToUpdate, $sAttCode, $value);
}
public function Delete(string $sClass, string $sId): void
{
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) {
/** @var DBObjectSearch $oFilter */
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
parent::Delete($sClass, $sId);
} catch (\Exception $e) {
DataFeatureRemovalLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
}
public function SetIssue(string $sClass): void
{
throw new DataFeatureRemovalException('Deletion Plan cannot be executed due to issues');
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
use DBObject;
/**
* Manage operation summary instead of doing the actual update or delete
*
* The summary is an array [class => DeletionPlanSummaryEntity]
*/
class ObjectServiceSummary implements iObjectService
{
private array $aSummary = [];
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void
{
$sClass = get_class($oToUpdate);
DataFeatureRemovalLog::Info('Update object', 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++;
}
public function Delete(string $sClass, string $sId): void
{
DataFeatureRemovalLog::Info('Delete object', null, ['class' => $sClass, 'id' => $sId]);
if (!array_key_exists($sClass, $this->aSummary)) {
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
$oDeletionPlanSummaryEntity->iDeleteCount++;
}
public function SetIssue(string $sClass): void
{
DataFeatureRemovalLog::Info('Issue on object', null, ['class' => $sClass]);
if (!array_key_exists($sClass, $this->aSummary)) {
$this->aSummary[$sClass] = new DataCleanupSummaryEntity($sClass);
}
$oDeletionPlanSummaryEntity = $this->aSummary[$sClass];
$oDeletionPlanSummaryEntity->iIssueCount++;
}
public function GetSummary(): array
{
return $this->aSummary;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DBObject;
interface iObjectService
{
public function Update(DBObject $oToUpdate, string $sAttCode, $value): void;
public function Delete(string $sClass, string $sId): void;
public function SetIssue(string $sClass): void;
public function GetSummary(): array;
}

View File

@@ -17,7 +17,7 @@
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }}
{{ 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s }}
{% endif %}
{% UIForm Standard {} %}

View File

@@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

View File

@@ -7,12 +7,15 @@ $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\\Entity\\DataCleanupSummaryEntity' => $baseDir . '/src/Entity/DataCleanupSummaryEntity.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',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataCleanupService' => $baseDir . '/src/Service/DataCleanupService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => $baseDir . '/src/Service/ObjectService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => $baseDir . '/src/Service/ObjectServiceSummary.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => $baseDir . '/src/Service/iObjectService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -7,5 +7,4 @@ $baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\' => array($baseDir . '/src'),
'' => array($baseDir . '/src/NoNamespace'),
);

View File

@@ -20,19 +20,18 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
),
);
public static $fallbackDirsPsr4 = array (
0 => __DIR__ . '/../..' . '/src/NoNamespace',
);
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\\Entity\\DataCleanupSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DataCleanupSummaryEntity.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',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataCleanupService' => __DIR__ . '/../..' . '/src/Service/DataCleanupService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => __DIR__ . '/../..' . '/src/Service/ObjectService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => __DIR__ . '/../..' . '/src/Service/ObjectServiceSummary.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => __DIR__ . '/../..' . '/src/Service/iObjectService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
@@ -41,7 +40,6 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$fallbackDirsPsr4;
$loader->classMap = ComposerStaticInit4f96a7199e2c0d90e547333758b26464::$classMap;
}, null, ClassLoader::class);

View File

@@ -0,0 +1,5 @@
{
"packages": [],
"dev": true,
"dev-package-names": []
}

View File

@@ -0,0 +1,23 @@
<?php return array(
'root' => array(
'name' => 'combodo/combodo-data-feature-removal',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19bbf6759bb4f6f5814d9ec1b0b5514208efc0b2',
'type' => 'itop-extension',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'combodo/combodo-data-feature-removal' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '19bbf6759bb4f6f5814d9ec1b0b5514208efc0b2',
'type' => 'itop-extension',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);