From 4582256f016a190bd6343ec710ce69127065f486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Thu, 30 Apr 2026 10:20:25 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B09165=20-=20secure=20data=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/utils.inc.php | 26 ++ .../composer.json | 3 +- .../en.dict.combodo-data-feature-removal.php | 4 +- .../fr.dict.combodo-data-feature-removal.php | 4 +- .../DataFeatureRemovalController.php | 17 +- ...ntity.php => DataCleanupSummaryEntity.php} | 10 +- .../src/Helper/DataFeatureRemovalConfig.php | 6 +- .../src/Service/DataCleanupService.php | 177 ++++++++ .../src/Service/DeletionPlanService.php | 182 -------- .../src/Service/ObjectService.php | 60 +++ .../src/Service/ObjectServiceSummary.php | 58 +++ .../src/Service/iObjectService.php | 21 + .../templates/DeletionPlan.html.twig | 2 +- .../vendor/composer/InstalledVersions.php | 396 ++++++++++++++++++ .../vendor/composer/autoload_classmap.php | 7 +- .../vendor/composer/autoload_psr4.php | 1 - .../vendor/composer/autoload_static.php | 12 +- .../vendor/composer/installed.json | 5 + .../vendor/composer/installed.php | 23 + lib/autoload.php | 5 +- lib/composer/autoload_classmap.php | 1 + lib/composer/autoload_psr4.php | 2 +- lib/composer/autoload_static.php | 5 +- lib/composer/platform_check.php | 5 +- .../unattended-install/unattended-install.php | 6 +- setup/wizardsteps/WizStepModulesChoice.php | 3 +- sources/Service/Limits/ExecutionLimits.php | 49 +++ .../php-static-analysis/config/base.dist.neon | 2 +- .../DataCleanupServiceTest.php | 301 +++++++++++++ .../DeletionPlanServiceTest.php | 277 ------------ .../data_cleanup_delta.xml | 346 +++++++++++++++ 31 files changed, 1505 insertions(+), 511 deletions(-) rename datamodels/2.x/combodo-data-feature-removal/src/Entity/{DeletionPlanSummaryEntity.php => DataCleanupSummaryEntity.php} (56%) create mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Service/DataCleanupService.php delete mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php create mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectService.php create mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php create mode 100644 datamodels/2.x/combodo-data-feature-removal/src/Service/iObjectService.php create mode 100644 datamodels/2.x/combodo-data-feature-removal/vendor/composer/InstalledVersions.php create mode 100644 datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json create mode 100644 datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php create mode 100644 sources/Service/Limits/ExecutionLimits.php create mode 100644 tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DataCleanupServiceTest.php delete mode 100644 tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DeletionPlanServiceTest.php create mode 100644 tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/data_cleanup_delta.xml diff --git a/application/utils.inc.php b/application/utils.inc.php index e19f7f18a3..915319c71b 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -3190,4 +3190,30 @@ TXT } } } + + /** + * Read memory limit from the php.ini file + * + * @return int Memory limit in bytes + */ + public static function GetMemoryLimit(): int + { + $sLimit = ini_get('memory_limit'); + if ($sLimit == '-1') { + return 128 * 1048576; + } + switch (substr($sLimit, -1)) { + case 'M': + case 'm': + return (int)$sLimit * 1048576; + case 'K': + case 'k': + return (int)$sLimit * 1024; + case 'G': + case 'g': + return (int)$sLimit * 1073741824; + default: + return (int)$sLimit; + } + } } diff --git a/datamodels/2.x/combodo-data-feature-removal/composer.json b/datamodels/2.x/combodo-data-feature-removal/composer.json index 4640f253f8..dca5174167 100644 --- a/datamodels/2.x/combodo-data-feature-removal/composer.json +++ b/datamodels/2.x/combodo-data-feature-removal/composer.json @@ -4,8 +4,7 @@ }, "autoload": { "psr-4": { - "Combodo\\iTop\\DataFeatureRemoval\\": "src", - "": "src/NoNamespace" + "Combodo\\iTop\\DataFeatureRemoval\\": "src" } }, "name": "combodo/combodo-data-feature-removal", 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 f919656908..e3f162a2a3 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 @@ -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', 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 d26e701cea..ff619ba4d5 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 @@ -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 d’entré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', 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 1917d47e34..f7059412d5 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 @@ -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); } diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanSummaryEntity.php b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php similarity index 56% rename from datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanSummaryEntity.php rename to datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php index b477827741..2bfe87f783 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanSummaryEntity.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DataCleanupSummaryEntity.php @@ -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; 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 index af4b8031a8..bb07f28ad8 100644 --- a/datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php +++ b/datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalConfig.php @@ -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) diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/DataCleanupService.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/DataCleanupService.php new file mode 100644 index 0000000000..e2d3956b7a --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/DataCleanupService.php @@ -0,0 +1,177 @@ +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; + } +} 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 deleted file mode 100644 index c0cce332b1..0000000000 --- a/datamodels/2.x/combodo-data-feature-removal/src/Service/DeletionPlanService.php +++ /dev/null @@ -1,182 +0,0 @@ - - * @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; - } -} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectService.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectService.php new file mode 100644 index 0000000000..e0c94b75a0 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectService.php @@ -0,0 +1,60 @@ +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'); + } + +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php new file mode 100644 index 0000000000..0c4b53f1c4 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/ObjectServiceSummary.php @@ -0,0 +1,58 @@ + 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; + } +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/iObjectService.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/iObjectService.php new file mode 100644 index 0000000000..f12831b74e --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/iObjectService.php @@ -0,0 +1,21 @@ + + * Jordi Boggiano + * + * 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}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + 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 + */ + 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 + */ + 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} + */ + 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}> + */ + 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} $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}> + */ + 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} $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} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} 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 97dffa124f..8a38a84587 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 @@ -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', ); diff --git a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_psr4.php b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_psr4.php index 3c66a5fbc2..a344fca477 100644 --- a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_psr4.php +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_psr4.php @@ -7,5 +7,4 @@ $baseDir = dirname($vendorDir); return array( 'Combodo\\iTop\\DataFeatureRemoval\\' => array($baseDir . '/src'), - '' => array($baseDir . '/src/NoNamespace'), ); 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 584484cfa7..94e5a3715f 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 @@ -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); diff --git a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json new file mode 100644 index 0000000000..87fda747e6 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": true, + "dev-package-names": [] +} diff --git a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php new file mode 100644 index 0000000000..e7242a61e0 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/installed.php @@ -0,0 +1,23 @@ + 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, + ), + ), +); diff --git a/lib/autoload.php b/lib/autoload.php index 1b6f5ac129..9861c4c24c 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -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'; diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 084c426979..50b73aabc9 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -625,6 +625,7 @@ return array( 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => $baseDir . '/sources/Service/Events/iEventServiceSetup.php', 'Combodo\\iTop\\Service\\Import\\CSVImportPageProcessor' => $baseDir . '/sources/Service/Import/CSVImportPageProcessor.php', 'Combodo\\iTop\\Service\\InterfaceDiscovery\\InterfaceDiscovery' => $baseDir . '/sources/Service/InterfaceDiscovery/InterfaceDiscovery.php', + 'Combodo\\iTop\\Service\\Limits\\ExecutionLimits' => $baseDir . '/sources/Service/Limits/ExecutionLimits.php', 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => $baseDir . '/sources/Service/Links/LinkSetDataTransformer.php', 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php', diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 22fc9f0a3b..be2d5f112e 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -65,7 +65,7 @@ return array( 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'), - 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'), + 'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 191491d7f0..738a3fe28a 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -360,8 +360,8 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 ), 'League\\OAuth2\\Client\\' => array ( - 0 => __DIR__ . '/..' . '/league/oauth2-client/src', - 1 => __DIR__ . '/..' . '/league/oauth2-google/src', + 0 => __DIR__ . '/..' . '/league/oauth2-google/src', + 1 => __DIR__ . '/..' . '/league/oauth2-client/src', ), 'GuzzleHttp\\Psr7\\' => array ( @@ -1026,6 +1026,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => __DIR__ . '/../..' . '/sources/Service/Events/iEventServiceSetup.php', 'Combodo\\iTop\\Service\\Import\\CSVImportPageProcessor' => __DIR__ . '/../..' . '/sources/Service/Import/CSVImportPageProcessor.php', 'Combodo\\iTop\\Service\\InterfaceDiscovery\\InterfaceDiscovery' => __DIR__ . '/../..' . '/sources/Service/InterfaceDiscovery/InterfaceDiscovery.php', + 'Combodo\\iTop\\Service\\Limits\\ExecutionLimits' => __DIR__ . '/../..' . '/sources/Service/Limits/ExecutionLimits.php', 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetDataTransformer.php', 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php', 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php', diff --git a/lib/composer/platform_check.php b/lib/composer/platform_check.php index bb733000d3..f6cf0ea27c 100644 --- a/lib/composer/platform_check.php +++ b/lib/composer/platform_check.php @@ -36,8 +36,7 @@ if ($issues) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } diff --git a/setup/unattended-install/unattended-install.php b/setup/unattended-install/unattended-install.php index 475a478fc2..c1d9e63b13 100644 --- a/setup/unattended-install/unattended-install.php +++ b/setup/unattended-install/unattended-install.php @@ -312,9 +312,6 @@ if ($bInstall) { } else { echo "\nFailed to record designer updates(".$oMysqli->error.").\n"; } - } else { - echo "\nFailed to read the revision from $sDeltaFile file. No designer update information will be recorded.\n"; - } } } @@ -349,8 +346,7 @@ if (! $bFoundIssues) { $sLogMsg = "installed!"; if ($bUseItopConfig && is_file("$sConfigFile.backup")) { - echo "\nuse config file provided by backup in $sConfigFile."; - copy("$sConfigFile.backup", $sConfigFile); + unlink("$sConfigFile.backup"); } SetupLog::Info($sLogMsg); diff --git a/setup/wizardsteps/WizStepModulesChoice.php b/setup/wizardsteps/WizStepModulesChoice.php index b6b5e8a9b9..802f0f6f96 100644 --- a/setup/wizardsteps/WizStepModulesChoice.php +++ b/setup/wizardsteps/WizStepModulesChoice.php @@ -18,7 +18,6 @@ * You should have received a copy of the GNU Affero General Public License */ -use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\PhpParser\Evaluation\PhpExpressionEvaluator; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader; use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReaderException; @@ -509,7 +508,7 @@ EOF $sDisplayChoices .= '
  • '.$aChoice['title'].'
  • '; if (isset($aChoice['modules'])) { if (count($aChoice['modules']) === 0) { - throw new Exception('Extension '.$aChoice['extension_code'].' does not have any module associated'); + //throw new Exception('Extension '.$aChoice['extension_code'].' does not have any module associated'); } foreach ($aChoice['modules'] as $sModuleId) { $bSelected = true; diff --git a/sources/Service/Limits/ExecutionLimits.php b/sources/Service/Limits/ExecutionLimits.php new file mode 100644 index 0000000000..c5343d856c --- /dev/null +++ b/sources/Service/Limits/ExecutionLimits.php @@ -0,0 +1,49 @@ +iMaxTime = ($iMaxDuration > 0) ? ($iMaxDuration + time()) : 0; + $this->iMaxMemoryPercent = (int)min(max($iMaxMemoryPercent, 0), 100); + } + /** + * @return bool true when duration is + */ + public function ShouldStopExecution(): bool + { + if (($this->iMaxTime != 0) && (time() > $this->iMaxTime)) { + \IssueLog::Debug(__METHOD__.' timeout '.time()." (current) > $this->iMaxTime (max)"); + return true; + } + + $iMaxMemoryPercent = (int)min(max($this->iMaxMemoryPercent, 0), 100); + $iMemory = memory_get_usage(true); + $iMaxMemory = utils::GetMemoryLimit() * $iMaxMemoryPercent / 100; + if ($iMemory > $iMaxMemory) { + \IssueLog::Debug(__METHOD__." Memory limit $iMemory (current) > $iMaxMemory (max)"); + return true; + } + + return false; + } + +} diff --git a/tests/php-static-analysis/config/base.dist.neon b/tests/php-static-analysis/config/base.dist.neon index 9afd95e84a..e1ac3d09c2 100644 --- a/tests/php-static-analysis/config/base.dist.neon +++ b/tests/php-static-analysis/config/base.dist.neon @@ -36,6 +36,6 @@ parameters: - ../../../env-php-unit-tests (?) # Irrelevant as it will either already be in `env-production` or might be desynchronized from `env-production` - ../../../env-toolkit (?) # Irrelevent as it will either already be in `env-production` or might be desynchronized from `env-production` (for local run only, not useful in the CI) - - ../../../tests (?) # Exclude tests for now + #- ../../../tests (?) # Exclude tests for now - ../../../toolkit (?) # Exclude toolkit for now - ../../../setup/compat # Exclude fake DOMDocument & DOMElement declarations diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DataCleanupServiceTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DataCleanupServiceTest.php new file mode 100644 index 0000000000..6846b7d59e --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DataCleanupServiceTest.php @@ -0,0 +1,301 @@ +GetCleanupSummary(null); + + $this->assertIsArray($aResult, 'Expected result to be an array when input is null.'); + $this->assertEmpty($aResult, 'Expected result to be empty array when input is null.'); + } + + //--- ExecuteCleanup tests --- + + public function testExecuteCleanup_DeleteOneObjPerClassWithoutLimit() + { + $this->GivenDFRTreeInDB(<<ExecuteCleanup($aClasses); + $aExpected = [ + ['DFRToUpdate', 1, 0 ], + ['DFRToRemoveLeaf', 0, 1 ], + ['DFRRemovedCollateral', 0, 1 ], + ['DFRRemovedCollateralCascade', 0, 1 ], + ]; + $this->AssertSummaryEquals($aExpected, $aRes); + } + + public function testExecuteCleanup_DeleteManyObjPerClassWithoutLimit() + { + $this->GivenDFRTreeInDB(<<ExecuteCleanup($aClasses); + $aExpected = [ + ['DFRToUpdate', 3, 0 ], + ['DFRToRemoveLeaf', 0, 3 ], + ['DFRRemovedCollateral', 0, 3 ], + ['DFRRemovedCollateralCascade', 0, 3 ], + ]; + $this->AssertSummaryEquals($aExpected, $aRes); + } + + public function testGetCleanupSummary_DeleteManyObjPerClassWithoutLimit() + { + $this->GivenDFRTreeInDB(<<GetCleanupSummary($aClasses); + $aExpected = [ + ['DFRToUpdate', 3, 0 ], + ['DFRToRemoveLeaf', 0, 3 ], + ['DFRRemovedCollateral', 0, 3 ], + ['DFRRemovedCollateralCascade', 0, 3 ], + ]; + $this->AssertSummaryEquals($aExpected, $aRes); + } + + public function testExecuteCleanup_ManualDeleteShouldFail() + { + $this->GivenDFRTreeInDB(<<expectException(DataFeatureRemovalException::class); + $this->expectExceptionMessage('Deletion Plan cannot be executed due to issues'); + $oService = new DataCleanupService(); + $oService->ExecuteCleanup($aClasses); + } + + public function testGetCleanupSummary_ManualDeleteShouldFail() + { + $this->GivenDFRTreeInDB(<<GetCleanupSummary($aClasses); + $aExpected = [ + ['DFRManual', 0, 0, 3 ], + ['DFRToRemoveLeaf', 0, 2], + ]; + $this->AssertSummaryEquals($aExpected, $aRes); + } + + private function AssertSummaryEquals(array $expected, $actual, $sMessage = '') + { + $aExpected = []; + foreach ($expected as $line) { + $sClass = $line[0]; + $iUpdate = $line[1]; + $iDelete = $line[2]; + $iIssue = $line[3] ?? 0; + + $oCleanupSummaryEntity = new DataCleanupSummaryEntity($sClass); + $oCleanupSummaryEntity->iUpdateCount = $iUpdate; + $oCleanupSummaryEntity->iDeleteCount = $iDelete; + $oCleanupSummaryEntity->iIssueCount = $iIssue; + $aExpected[$sClass] = $oCleanupSummaryEntity; + } + $this->assertEquals($aExpected, $actual, $sMessage); + } + + public static function ExecuteCleanup_StopInProcessKeepDatabaseOk(): array + { + return [ + 'Stop after 1' => [ + 1, + [ + ['DFRToUpdate', 1, 0], + ], + ], + 'Stop after 2' => [ + 2, + [ + ['DFRToUpdate', 1, 0], + ['DFRRemovedCollateralCascade', 0, 1], + ], + ], + 'Stop after 3' => [ + 3, + [ + ['DFRToUpdate', 1, 0], + ['DFRRemovedCollateralCascade', 0, 1], + ['DFRRemovedCollateral', 0, 1], + ], + ], + 'Stop after 4' => [ + 4, + [ + ['DFRToUpdate', 1, 0], + ['DFRRemovedCollateralCascade', 0, 1], + ['DFRRemovedCollateral', 0, 1], + ['DFRToRemoveLeaf', 0, 1], + ], + ], + 'Stop after 5' => [ + 5, + [ + ['DFRToUpdate', 2, 0], + ['DFRRemovedCollateralCascade', 0, 1], + ['DFRRemovedCollateral', 0, 1], + ['DFRToRemoveLeaf', 0, 1], + ], + ], + ]; + } + + /** + * @dataProvider ExecuteCleanup_StopInProcessKeepDatabaseOk + */ + public function testExecuteCleanup_StopInProcessKeepDatabaseOk(int $iExecutionCount, array $aExpected): void + { + $this->GivenDFRTreeInDB(<<GivenExecutionLimits($iExecutionCount); + $this->SetNonPublicProperty($oDeletionPlaService, 'oExecutionLimits', $this->oExecutionLimits); + $aRes = $oDeletionPlaService->ExecuteCleanup($aClasses); + $this->AssertSummaryEquals($aExpected, $aRes); + } + + private function GivenDFRTreeInDB(string $sTree) + { + $aTree = explode("\n", $sTree); + foreach ($aTree as $sLine) { + if (trim($sLine) === "") { + continue; + } + $this->GivenDFRTreeLineInDB($sLine); + } + } + + private array $aIdByObjectName = []; + private function GivenDFRTreeLineInDB(string $sLine) + { + list($sLeft, $sRight) = explode('<-', $sLine); + $sLeft = trim($sLeft); + + $iLeftId = $this->aIdByObjectName[$sLeft] ?? 0; + if ($iLeftId === 0) { + list($sChildClass, ) = explode('_', $sLeft, 2); + $iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]); + $this->aIdByObjectName[$sLeft] = $iLeftId; + } + + $sRight = trim($sRight); + list($sChildClass, ) = explode('_', $sRight, 2); + $iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sRight, 'extkey_id' => $iLeftId]); + $this->aIdByObjectName[$sRight] = $iRightId; + } + + private function GivenExecutionLimits(int $iStopAfterCallNumberReached): void + { + $matcher = $this->any(); + + $this->oExecutionLimits = $this->createMock(ExecutionLimits::class); + $this->oExecutionLimits->expects($matcher) + ->method('ShouldStopExecution')->willReturnCallback(function () use ($matcher, $iStopAfterCallNumberReached) { + $invocationCount = $matcher->getInvocationCount(); + + return ($invocationCount >= $iStopAfterCallNumberReached); + }); + } +} diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DeletionPlanServiceTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DeletionPlanServiceTest.php deleted file mode 100644 index 4d78905208..0000000000 --- a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/DeletionPlanServiceTest.php +++ /dev/null @@ -1,277 +0,0 @@ -RequireOnceItopFile('env-production/combodo-data-feature-removal/vendor/autoload.php'); - } - - //--- GetDeletionPlanSummary tests --- - - /** - * Tests that GetDeletionPlanSummary returns an empty array when passed null as input. - */ - public function testGetDeletionPlanSummaryReturnsEmptyArrayWhenNull(): void - { - $oService = DeletionPlanService::GetInstance(); - $aResult = $oService->GetDeletionPlanSummary(null); - - $this->assertIsArray($aResult, 'Expected result to be an array when input is null.'); - $this->assertEmpty($aResult, 'Expected result to be empty array when input is null.'); - } - - /** - * Tests that GetDeletionPlanSummary returns an empty array when the input class list is empty. - */ - public function testGetDeletionPlanSummaryReturnsEmptyArrayWhenEmptyClasses(): void - { - $oMockService = $this->getMockBuilder(DeletionPlanService::class) - ->disableOriginalConstructor() - ->onlyMethods(['GetDeletionPlan']) - ->getMock(); - - $oDeletionPlan = new DeletionPlan(); - $oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan); - - DeletionPlanService::SetInstance($oMockService); - - $aResult = $oMockService->GetDeletionPlanSummary([]); - - $this->assertIsArray($aResult, 'Expected result to be an array when input class list is empty.'); - $this->assertEmpty($aResult, 'Expected result to be empty array when input class list is empty.'); - } - - /** - * Tests GetDeletionPlanSummary for various delete/update scenarios using a data provider. - * Verifies summary output for each class matches expected values. - * - * @dataProvider GetDeletionPlanSummaryWithDeletesProvider - */ - public function testGetDeletionPlanSummaryWithDeletes(array $aToDelete, array $aToUpdate, array $aExpected): void - { - $oDeletionPlan = $this->createMock(DeletionPlan::class); - $oDeletionPlan->method('ListDeletes')->willReturn($aToDelete); - $oDeletionPlan->method('ListUpdates')->willReturn($aToUpdate); - - $oMockService = $this->getMockBuilder(DeletionPlanService::class) - ->disableOriginalConstructor() - ->onlyMethods(['GetDeletionPlan']) - ->getMock(); - - $oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan); - - $aResult = $oMockService->GetDeletionPlanSummary(['SomeClass']); - - foreach ($aExpected as $sClass => $aExpectedValues) { - $this->assertArrayHasKey( - $sClass, - $aResult, - "Expected key '$sClass' to exist in summary." - ); - $this->assertInstanceOf( - DeletionPlanSummaryEntity::class, - $aResult[$sClass], - "Expected summary for '$sClass' to be instance of DeletionPlanSummaryEntity." - ); - $this->assertEquals( - $aExpectedValues['iDeleteCount'], - $aResult[$sClass]->iDeleteCount, - "Expected iDeleteCount for '$sClass' to be {$aExpectedValues['iDeleteCount']}, got {$aResult[$sClass]->iDeleteCount}." - ); - $this->assertEquals( - $aExpectedValues['iUpdateCount'], - $aResult[$sClass]->iUpdateCount, - "Expected iUpdateCount for '$sClass' to be {$aExpectedValues['iUpdateCount']}, got {$aResult[$sClass]->iUpdateCount}." - ); - $this->assertEquals( - $aExpectedValues['iMode'], - $aResult[$sClass]->iMode, - "Expected iMode for '$sClass' to be {$aExpectedValues['iMode']}, got {$aResult[$sClass]->iMode}." - ); - $this->assertEquals( - $aExpectedValues['sIssue'], - $aResult[$sClass]->sIssue, - "Expected sIssue for '$sClass' to be '{$aExpectedValues['sIssue']}', got '{$aResult[$sClass]->sIssue}'." - ); - } - } - - /** - * Provides multiple scenarios for testGetDeletionPlanSummaryWithDeletes, including deletes, updates, issues, and multiple classes. - * - * @return array - */ - public function GetDeletionPlanSummaryWithDeletesProvider(): array - { - return [ - 'single class with deletes only' => [ - 'aToDelete' => [ - 'Server' => [ - 1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true], - 2 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false], - ], - ], - 'aToUpdate' => [], - 'aExpected' => [ - 'Server' => [ - 'iDeleteCount' => 2, - 'iUpdateCount' => 0, - 'iMode' => DEL_AUTO, - 'sIssue' => null, - ], - ], - ], - 'single class with deletes and issue' => [ - 'aToDelete' => [ - 'Server' => [ - 1 => ['to_delete' => null, 'mode' => DEL_MANUAL, 'requested_explicitely' => false, 'issue' => 'Cannot delete'], - ], - ], - 'aToUpdate' => [], - 'aExpected' => [ - 'Server' => [ - 'iDeleteCount' => 1, - 'iUpdateCount' => 0, - 'iMode' => DEL_MANUAL, - 'sIssue' => 'Cannot delete', - ], - ], - ], - 'single class with updates only' => [ - 'aToDelete' => [], - 'aToUpdate' => [ - 'Person' => [ - 10 => ['to_reset' => null, 'attributes' => ['org_id' => null]], - 11 => ['to_reset' => null, 'attributes' => ['org_id' => null]], - 12 => ['to_reset' => null, 'attributes' => ['org_id' => null]], - ], - ], - 'aExpected' => [ - 'Person' => [ - 'iDeleteCount' => 0, - 'iUpdateCount' => 3, - 'iMode' => 0, - 'sIssue' => null, - ], - ], - ], - 'class with both deletes and updates' => [ - 'aToDelete' => [ - 'Server' => [ - 1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true], - ], - ], - 'aToUpdate' => [ - 'Server' => [ - 5 => ['to_reset' => null, 'attributes' => ['org_id' => null]], - ], - ], - 'aExpected' => [ - 'Server' => [ - 'iDeleteCount' => 1, - 'iUpdateCount' => 1, - 'iMode' => DEL_AUTO, - 'sIssue' => null, - ], - ], - ], - 'multiple classes' => [ - 'aToDelete' => [ - 'Server' => [ - 1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true], - ], - 'NetworkDevice' => [ - 3 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false], - 4 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false], - ], - ], - 'aToUpdate' => [ - 'Person' => [ - 10 => ['to_reset' => null, 'attributes' => ['org_id' => null]], - ], - ], - 'aExpected' => [ - 'Server' => [ - 'iDeleteCount' => 1, - 'iUpdateCount' => 0, - 'iMode' => DEL_AUTO, - 'sIssue' => null, - ], - 'NetworkDevice' => [ - 'iDeleteCount' => 2, - 'iUpdateCount' => 0, - 'iMode' => DEL_SILENT, - 'sIssue' => null, - ], - 'Person' => [ - 'iDeleteCount' => 0, - 'iUpdateCount' => 1, - 'iMode' => 0, - 'sIssue' => null, - ], - ], - ], - ]; - } - - //--- ExecuteDeletionPlan tests --- - - /** - * Tests that ExecuteDeletionPlan throws a DataFeatureRemovalException when the deletion plan contains issues. - */ - public function testExecuteDeletionPlanThrowsExceptionWhenIssuesExist(): void - { - $this->RequireOnceItopFile('env-production/combodo-data-feature-removal/src/Helper/DataFeatureRemovalException.php'); - - $oDeletionPlan = $this->createMock(DeletionPlan::class); - $oDeletionPlan->method('GetIssues')->willReturn(['Some issue']); - $oDeletionPlan->method('ListDeletes')->willReturn([]); - $oDeletionPlan->method('ListUpdates')->willReturn([]); - - $oMockService = $this->getMockBuilder(DeletionPlanService::class) - ->disableOriginalConstructor() - ->onlyMethods(['GetDeletionPlan']) - ->getMock(); - - $oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan); - - $this->expectException(DataFeatureRemovalException::class); - $this->expectExceptionMessage('Deletion Plan cannot be executed due to issues'); - - $oMockService->ExecuteDeletionPlan(['SomeClass']); - } -} diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/data_cleanup_delta.xml b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/data_cleanup_delta.xml new file mode 100644 index 0000000000..a338e1fbc5 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/data_cleanup_delta.xml @@ -0,0 +1,346 @@ + + + + + + bizmodel,searchable + true + dfrtoremove + + + + + + + + + + name + + true + + + all + + + + + + + + + + +
    + + + 10 + + +
    +
    + cmdbAbstractObject +
    + + + bizmodel,searchable + false + dfrtoupdate + + + + + + + + + + name + + true + + + all + + + extkey_id + + + true + DFRToRemove + DEL_AUTO + all + + + + + + + + + + +
    + + + 10 + + + 20 + + +
    +
    + cmdbAbstractObject +
    + + + bizmodel,searchable + false + dfrremovedcollateral + + + + + + + + + + name + + true + + + all + + + extkey_id + + + false + DFRToRemove + DEL_AUTO + all + + + + + + + + + + +
    + + + 10 + + + 20 + + +
    +
    + cmdbAbstractObject +
    + + + bizmodel,searchable + false + dfrtoremoveleaf + + + + + + + + + + desc + + true + + + all + + + + + + + + + + +
    + + + 10 + + + 20 + + +
    +
    + DFRToRemove +
    + + + bizmodel,searchable + false + dfrremovedcollateralcascade + + + + + + + + + + name + + true + + + all + + + extkey_id + + + false + DFRRemovedCollateral + DEL_AUTO + all + + + + + + + + + + +
    + + + 10 + + + 20 + + +
    +
    + cmdbAbstractObject +
    + + + bizmodel,searchable + false + dfrmanual + + + + + + + + + + name + + true + + + all + + + extkey_id + + + false + DFRToRemove + DEL_MANUAL + all + + + + + + + + + + +
    + + + 10 + + + 20 + + +
    +
    + cmdbAbstractObject +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +