diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index 8b17a931db..f5698f647c 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -916,7 +916,9 @@ public static function QueryToCol($sSql, $col) $aColumn = []; $aData = self::QueryToArray($sSql); foreach ($aData as $aRow) { - @$aColumn[] = $aRow[$col]; + if ($aRow[$col] !== null) { + $aColumn[] = $aRow[$col]; + } } return $aColumn; } 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 1bbe4f0191..53c67d856d 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 @@ -18,6 +18,7 @@ use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog; use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService; use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService; +use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan; use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment; use Combodo\iTop\Setup\FeatureRemoval\SetupAudit; use ContextTag; @@ -222,6 +223,7 @@ private function Compile(array $aAddedExtensions, array $aRemovedExtensions, boo $bIsDirEmpty = count(scandir($sBuildDir)) === 2; if ($bIsDirEmpty || $bForceCompilation) { + Session::Unset('bForceCompilation'); DataFeatureRemovalLog::Debug( __METHOD__, null, @@ -272,7 +274,7 @@ private function GetExecutionSummaryTable(): array private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array { $sName = 'DeletionPlanSummary'; - $oDataCleanupService = new DataCleanupService(); + $oDataCleanupService = new StaticDeletionPlan(); $aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses); $aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count']; $aRows = []; diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanEntity.php b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanEntity.php new file mode 100644 index 0000000000..bd001e0400 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanEntity.php @@ -0,0 +1,37 @@ +oDelete = $oDelete ?? new DeletionPlanItem(); + $this->oUpdate = $oUpdate ?? new DeletionPlanItem(); + $this->oIssue = $oIssue ?? new DeletionPlanItem(); + } + + public function TotalCount(): int + { + return $this->oDelete->Count() + $this->oUpdate->Count() + $this->oIssue->Count(); + } + + public function FilterUpdatesByDeletes() + { + $this->oUpdate->FilterBy($this->oDelete); + } +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanItem.php b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanItem.php new file mode 100644 index 0000000000..1f3d64d835 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Entity/DeletionPlanItem.php @@ -0,0 +1,36 @@ +aIds = $aIds; + } + + public function Merge(DeletionPlanItem $oItem): void + { + $this->aIds = array_unique(array_merge($this->aIds, $oItem->aIds)); + } + + public function Count(): int + { + return count($this->aIds); + } + + public function FilterBy(DeletionPlanItem $oItem): void + { + $this->aIds = array_diff($this->aIds, $oItem->aIds); + } +} diff --git a/datamodels/2.x/combodo-data-feature-removal/src/Service/StaticDeletionPlan.php b/datamodels/2.x/combodo-data-feature-removal/src/Service/StaticDeletionPlan.php new file mode 100644 index 0000000000..82cf651570 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/StaticDeletionPlan.php @@ -0,0 +1,219 @@ + */ + private array $aDeletionPlan = []; + + /** + * 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 + */ + public function GetCleanupSummary(?array $aClasses): array + { + $aSummary = []; + $aDeletionPlan = $this->GetStaticDeletionPlan($aClasses ?? []); + + foreach ($aDeletionPlan as $sClass => $oDeletionPlanEntity) { + if ($oDeletionPlanEntity->TotalCount() === 0) { + continue; + } + $oDeletionPlanEntity->FilterUpdatesByDeletes(); + $oDataCleanupSummary = new DataCleanupSummaryEntity($sClass); + $oDataCleanupSummary->iUpdateCount = $oDeletionPlanEntity->oUpdate->Count(); + $oDataCleanupSummary->iDeleteCount = $oDeletionPlanEntity->oDelete->Count(); + $oDataCleanupSummary->iIssueCount = $oDeletionPlanEntity->oIssue->Count(); + + $aSummary[$sClass] = $oDataCleanupSummary; + } + + return $aSummary; + } + + /** + * @param array $aClasses Classes to clean entirely + * + * @return array ['class' => DeletionPlanEntity]; + * + * @throws \CoreException + */ + public function GetStaticDeletionPlan(array $aClasses): array + { + foreach ($aClasses as $sClass) { + $oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass); + $oDeletionPlanEntity = new DeletionPlanEntity(); + $oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem); + $this->aDeletionPlan[$sClass] = $oDeletionPlanEntity; + + $this->DeletionPlanForReferencingClasses($sClass); + } + + return $this->aDeletionPlan; + } + + private function DeletionPlanForReferencingClasses(string $sClass): void + { + $sIdsToRemove = implode(', ', $this->aDeletionPlan[$sClass]->oDelete->aIds); + $aReferencingMe = MetaModel::EnumReferencingClasses($sClass); + foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) { + if (!isset($this->aDeletionPlan[$sRemoteClass])) { + $this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity(); + } + $oDeletionPlanEntity = $this->aDeletionPlan[$sRemoteClass]; + /** @var \AttributeExternalKey $oExtKeyAttDef */ + foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) { + // skip if this external key is behind an external field + if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) { + continue; + } + + if ($oExtKeyAttDef->IsNullAllowed()) { + // update + $oUpdateItem = $this->UpdateExtKeyNullable($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove); + $oDeletionPlanEntity->oUpdate->Merge($oUpdateItem); + } else { + // delete + $aRemoteIdsToRemove = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove); + + $iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption(); + if ($iDeletePropagationOption == DEL_MANUAL) { + // Issue, do not recurse + $oDeletionPlanItem = new DeletionPlanItem(aIds: $aRemoteIdsToRemove); + $oDeletionPlanEntity->oIssue->Merge($oDeletionPlanItem); + continue; + } + + if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) { + // update hierarchical keys due to row cleanup in the same table + $sTargetIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]->oDelete->aIds); + $oUpdateItem = $this->UpdateHierarchicalExtKey($sRemoteClass, $sExtKeyAttCode, $sTargetIdsToRemove); + $oDeletionPlanEntity->oUpdate->Merge($oUpdateItem); + } + + // Delete entries in Remote Class + if (count($aRemoteIdsToRemove) !== 0) { + $oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove)); + // Infinite loops do not occurs due to the datamodel structure + $this->DeletionPlanForReferencingClasses($sRemoteClass); + } + } + } + } + } + + /** + * @param string $sRemoteClass + * @param string $sExtKeyAttCode + * @param string $sIdsToRemoveInTargetClass + * + * @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem + * @throws \CoreException + */ + public function UpdateExtKeyNullable(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem + { + $aIds = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemoveInTargetClass); + + return new DeletionPlanItem($aIds); + } + + /** + * @param string $sRemoteClass + * @param string $sExtKeyAttCode + * @param string $sIdsToRemoveInTargetClass + * + * @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem + * @throws \CoreException + * @throws \MySQLException + */ + public function UpdateHierarchicalExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem + { + [$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode); + + $sSQL = <<GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode); + $sSQL = "SELECT `$sDBKey` FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)"; + + return CMDBSource::QueryToCol($sSQL, $sDBKey); + } + + /** + * @param string $sClass + * + * @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem + * @throws \CoreException + * @throws \MySQLException + */ + public function GetInitialClassDeletionPlan(string $sClass): DeletionPlanItem + { + $sTable = MetaModel::DBGetTable($sClass); + $sDBKey = MetaModel::DBGetKey($sClass); + $sSQL = "SELECT `$sDBKey` FROM `$sTable`"; + $aIds = CMDBSource::QueryToCol($sSQL, $sDBKey); + + return new DeletionPlanItem($aIds); + } + + /** + * Get database table for an attcode + * + * @param string $sClass + * @param string $sExtKeyAttCode + * + * @return array + * @throws \CoreException + * @throws \Exception + */ + public function GetDBInfoForAttcode(string $sClass, string $sExtKeyAttCode): array + { + $sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode); + $sDBTable = MetaModel::DBGetTable($sOriginClass); + $oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode); + // External key is on a single DB column + $sDBField = array_keys($oAttDef->GetSQLColumns())[0]; + $sDBKey = MetaModel::DBGetKey($sClass); + return [$sDBTable, $sDBField, $sDBKey]; + } + +} 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 8a38a84587..b8d4886620 100644 --- a/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php +++ b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_classmap.php @@ -8,6 +8,8 @@ return array( 'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php', 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => $baseDir . '/src/Entity/DataCleanupSummaryEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => $baseDir . '/src/Entity/DeletionPlanEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => $baseDir . '/src/Entity/DeletionPlanItem.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', @@ -16,6 +18,7 @@ 'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.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\\StaticDeletionPlan' => $baseDir . '/src/Service/StaticDeletionPlan.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_static.php b/datamodels/2.x/combodo-data-feature-removal/vendor/composer/autoload_static.php index 94e5a3715f..a86266cd44 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 @@ -23,6 +23,8 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464 public static $classMap = array ( 'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php', 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DataCleanupSummaryEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanEntity.php', + 'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanItem.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', @@ -31,6 +33,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464 'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.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\\StaticDeletionPlan' => __DIR__ . '/../..' . '/src/Service/StaticDeletionPlan.php', 'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => __DIR__ . '/../..' . '/src/Service/iObjectService.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', ); diff --git a/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php b/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php index 7a100ed28a..8788f6e4d1 100644 --- a/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php +++ b/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php @@ -242,8 +242,7 @@ public function OperationLaunchSetup() throw new SecurityException('Access forbidden'); } - $sConfigFile = APPCONF.'production/config-itop.php'; - @chmod($sConfigFile, 0770); // Allow overwriting the file + SetupUtils::CreateSetupToken(); header('Location: ../setup/'); } diff --git a/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php b/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php index 259dbf2625..ca85ca22b8 100644 --- a/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php +++ b/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php @@ -166,8 +166,6 @@ public function OperationUpdateCoreFiles() public function OperationRunSetup() { SetupUtils::CheckSetupToken(true); - $sConfigFile = APPCONF.'production/'.ITOP_CONFIG_FILE; - @chmod($sConfigFile, 0770); $sRedirectURL = utils::GetAbsoluteUrlAppRoot().'setup/index.php'; header("Location: $sRedirectURL"); } diff --git a/setup/runtimeenv.class.inc.php b/setup/runtimeenv.class.inc.php index 758cc92a91..b7cd585739 100644 --- a/setup/runtimeenv.class.inc.php +++ b/setup/runtimeenv.class.inc.php @@ -1344,7 +1344,7 @@ public function DataToCleanupAudit() $iCount = $oSetupAudit->GetDataToCleanupCount(); if ($iCount > 0) { - throw new Exception("$iCount elements require data adjustments or cleanup in the backoffice", DataAuditSequencer::DATA_AUDIT_FAILED); + throw new Exception("Elements require data adjustments or cleanup in the backoffice", DataAuditSequencer::DATA_AUDIT_FAILED); } } diff --git a/setup/wizard.php b/setup/wizard.php index b1f6e4c0dd..d1c7040ce7 100644 --- a/setup/wizard.php +++ b/setup/wizard.php @@ -74,7 +74,6 @@ function json_decode($json, $assoc = null) // The configuration file already exists if (!is_writable($sConfigFile)) { SetupUtils::ExitReadOnlyMode(false); // Reset readonly mode in case of problem - SetupUtils::EraseSetupToken(); $sRelativePath = utils::GetConfigFilePathRelative(ITOP_DEFAULT_ENV); $oP = new SetupPage('Installation Cannot Continue'); $oP->add("

Fatal error

\n"); @@ -87,7 +86,6 @@ function json_decode($json, $assoc = null) $oP->p($sButtonsHtml); $oP->output(); - // Prevent token creation exit; } else { chmod($sConfigFile, 0440); diff --git a/setup/wizardcontroller.class.inc.php b/setup/wizardcontroller.class.inc.php index 3015b35a69..76ba0bd587 100644 --- a/setup/wizardcontroller.class.inc.php +++ b/setup/wizardcontroller.class.inc.php @@ -196,7 +196,6 @@ protected function DisplayStep(WizardStep $oStep): void SetupLog::Info("=== Setup screen: ".$oStep->GetTitle().' ('.get_class($oStep).')'); $oPage = new SetupPage($oStep->GetTitle()); $oPage->LinkScriptFromAppRoot('setup/setup.js'); - $oStep->PreFormDisplay($oPage); $oPage->add('
'); $oPage->add('
'); diff --git a/setup/wizardsteps/WizStepWelcome.php b/setup/wizardsteps/WizStepWelcome.php index c9b878eb80..31ad75f12f 100644 --- a/setup/wizardsteps/WizStepWelcome.php +++ b/setup/wizardsteps/WizStepWelcome.php @@ -124,18 +124,18 @@ public function PostFormDisplay(SetupPage $oPage) if (file_exists($sBuildConfigFile)) { $oPage->add( << + - - - - -
HTML ); + $oPage->add_ready_script( + <<'); +JS + ); } } } diff --git a/setup/wizardsteps/WizardStep.php b/setup/wizardsteps/WizardStep.php index cc9f2190d8..a953c52756 100644 --- a/setup/wizardsteps/WizardStep.php +++ b/setup/wizardsteps/WizardStep.php @@ -76,10 +76,6 @@ public function PostFormDisplay(SetupPage $oPage) { } - public function PreFormDisplay(SetupPage $oPage) - { - } - protected function CheckDependencies() { if (is_null($this->bDependencyCheck)) { diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/AbstractCleanup.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/AbstractCleanup.php new file mode 100644 index 0000000000..aabf98152c --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/AbstractCleanup.php @@ -0,0 +1,127 @@ +aIdByClass = []; + $aTree = explode("\n", $sTree); + foreach ($aTree as $sLine) { + if (trim($sLine) === '') { + continue; + } + $this->GivenDFRTreeLineInDB($sLine); + } + } + + protected function GivenDFRTreeLineInDB(string $sLine) + { + [$sLeft, $sRight] = explode('<-', $sLine); + $sLeft = trim($sLeft); + + $iLeftId = $this->aIdByObjectName[$sLeft] ?? 0; + if ($iLeftId === 0) { + [$sChildClass] = explode('_', $sLeft, 2); + $iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]); + $this->aIdByClass[$sChildClass][] = $iLeftId; + $this->aIdByObjectName[$sLeft] = $iLeftId; + } + + $sRight = trim($sRight); + if (preg_match("/(?(?[^_]+)_\d+)(\s+\((?\w+)\))?/", $sRight, $aMatches) !== false) { + $sName = $aMatches['name']; + $sChildClass = $aMatches['class']; + $sExtKey = $aMatches['extkey'] ?? 'extkey_id'; + + $iRightId = $this->aIdByObjectName[$sName] ?? 0; + if ($iRightId === 0) { + $iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sName, $sExtKey => $iLeftId]); + $this->aIdByClass[$sChildClass][] = $iRightId; + $this->aIdByObjectName[$sName] = $iRightId; + } else { + // Update object + $oObj = MetaModel::GetObject($sChildClass, $iRightId); + $oObj->Set($sExtKey, $iLeftId); + $oObj->DBUpdate(); + } + } + } + + /** + * format of object: + * [create|update] CLASS (name = NAME, ...) + * + * @param string $sObjects + * + * @return void + */ + protected function GivenDFRObjectsInDB(string $sObjects) + { + $this->aIdByClass = []; + $sObjects = explode("\n", $sObjects); + foreach ($sObjects as $sLine) { + $sLine = trim($sLine); + if ($sLine === '') { + continue; + } + $this->GivenDFRObjectLineInDB($sLine); + } + } + + protected function GivenDFRObjectLineInDB(string $sLine) + { + if (preg_match("/(?\w+)\s+(?\w+)\s*\((?[^)]+)\)/", $sLine, $aMatches) !== false) { + $sVerb = $aMatches['verb']; + $sClass = $aMatches['class']; + $sAttList = $aMatches['att_list']; + + $aAttSet = explode(',', $sAttList); + $aAttributes = []; + foreach ($aAttSet as $sAtt) { + [$sAttName, $sAttValue] = explode('=', $sAtt, 2); + $sAttName = trim($sAttName); + $sAttValue = trim($sAttValue); + $aAttributes[$sAttName] = $sAttValue; + } + + $sName = $aAttributes['name'] ?? ''; + // Transform names in external keys into ids + foreach ($aAttributes as $sAttName => $sAttValue) { + if ($sAttName !== 'name') { + $aAttributes[$sAttName] = $this->aIdByObjectName[$sAttValue] ?? $sAttValue; + } + } + + switch ($sVerb) { + case 'create': + $sId = $this->GivenObjectInDB($sClass, $aAttributes); + $this->aIdByClass[$sClass][] = $sId; + $this->aIdByObjectName[$sName] = $sId; + break; + case 'update': + $sId = $this->aIdByObjectName[$sName]; + $oObj = MetaModel::GetObject($sClass, $sId); + foreach ($aAttributes as $sAttName => $sAttValue) { + $oObj->Set($sAttName, $sAttValue); + } + $oObj->DBUpdate(); + } + } + } + +} 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 index 6acb5f026c..0d3d04062c 100644 --- 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 @@ -34,15 +34,10 @@ * @see DataCleanupSummaryEntity * @see ItopDataTestCase */ -class DataCleanupServiceTest extends ItopCustomDatamodelTestCase +class DataCleanupServiceTest extends \AbstractCleanup { private ExecutionLimits&MockObject $oExecutionLimits; - public function GetDatamodelDeltaAbsPath(): string - { - return __DIR__.'/data_cleanup_delta.xml'; - } - //--- GetCleanupSummary tests --- /** @@ -261,36 +256,6 @@ public function testExecuteCleanup_StopInProcessKeepDatabaseOk(int $iExecutionCo $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) - { - [$sLeft, $sRight] = explode('<-', $sLine); - $sLeft = trim($sLeft); - - $iLeftId = $this->aIdByObjectName[$sLeft] ?? 0; - if ($iLeftId === 0) { - [$sChildClass, ] = explode('_', $sLeft, 2); - $iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]); - $this->aIdByObjectName[$sLeft] = $iLeftId; - } - - $sRight = trim($sRight); - [$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(); diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/StaticDeletionPlanTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/StaticDeletionPlanTest.php new file mode 100644 index 0000000000..643974c95f --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/StaticDeletionPlanTest.php @@ -0,0 +1,221 @@ +GivenDFRTreeInDB(<<GetInitialClassDeletionPlan('DFRToRemoveLeaf'); + self::assertEquals(2, $oDeletionPlanItem->Count(), 'All entries of root table should be removed'); + self::assertEquals($this->aIdByClass['DFRToRemoveLeaf'], $oDeletionPlanItem->aIds, 'All the Ids found in root table should correspond to the one created'); + } + + public function testUpdateExtKeyNullable() + { + $this->GivenDFRTreeInDB(<<UpdateExtKeyNullable( + $sRemoteClass, + 'extkey_id', + implode(',', $this->aIdByClass['DFRToRemoveLeaf']) + ); + + // THEN + self::assertEquals(3, $oDeletionPlanItem->Count(), 'All entries of root table should be removed'); + $sIdsToRemoveInTargetClass = implode(',', $this->aIdByClass['DFRToRemoveLeaf']); + $aExpectedIds = $oService->GetRemoteIdsForExtKey($sRemoteClass, 'extkey_id', $sIdsToRemoveInTargetClass); + + self::assertEquals($aExpectedIds, $oDeletionPlanItem->aIds, 'All entries pointing on root class should be removed'); + } + + /** + * Tests that GetCleanupSummary returns an empty array when passed null as input. + */ + public function testGetCleanupSummaryReturnsEmptyArrayWhenNull(): void + { + $oService = new StaticDeletionPlan(); + $aResult = $oService->GetStaticDeletionPlan([]); + + $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.'); + } + + public function testGetStaticDeletionPlan_DeleteObjRecursively() + { + $this->GivenDFRTreeInDB(<<GetStaticDeletionPlan($aClasses); + + self::assertArrayHasKey('DFRRemovedCollateralCascade', $aRes, 'The cleanup should descend to the cascaded classes'); + + // echo json_encode($aRes, JSON_PRETTY_PRINT)."\n"; + // echo json_encode($this->aIdByClass, JSON_PRETTY_PRINT); + } + + public function testGetStaticDeletionPlan_IssuesArePresent() + { + $this->GivenDFRTreeInDB(<<GetStaticDeletionPlan($aClasses); + + self::assertEquals(1, $aRes['DFRManual']->oIssue->Count(), 'Issue should be found because of DEL_MANUAL deletion policy'); + self::assertEquals($this->aIdByClass['DFRManual'], $aRes['DFRManual']->oIssue->aIds, 'Issue should be correspond to the entries created'); + } + + public function testGetStaticDeletionPlan_UpdateMultipleExtKeys() + { + $this->GivenDFRTreeInDB(<<GetStaticDeletionPlan($aClasses); + + self::assertArrayHasKey('DFRToUpdate', $aRes, 'Class to update should be targeted'); + self::assertEquals(2, $aRes['DFRToUpdate']->oUpdate->Count(), 'Update should be counted only for removed pointed classes'); + } + + public function testGetCleanupSummaryWithIssues() + { + $this->GivenDFRObjectsInDB(<<GetCleanupSummary($aClasses); + + $aExpected = [ + 'DFRToRemoveLeaf' => ['iDeleteCount' => 1], + 'DFRRemovedCollateral' => ['iDeleteCount' => 1], + 'DFRRemovedCollateralCascade' => ['iDeleteCount' => 2], + 'DFRToUpdate' => ['iUpdateCount' => 1], + 'DFRManual' => ['iIssueCount' => 1], + ]; + + $this->AssertSummaryEquals($aExpected, $aRes); + } + + public function testCircularRefsShouldNotRunInfinitely() + { + $this->GivenDFRObjectsInDB(<<GetCleanupSummary($aClasses); + + $aExpected = [ + 'DFRToRemoveLeaf' => ['iDeleteCount' => 1], + 'DFRRemovedCollateral' => ['iDeleteCount' => 1], + 'DFRCircularRefs' => ['iDeleteCount' => 1], + ]; + + $this->AssertSummaryEquals($aExpected, $aRes); + } + + public function testMultipleExtKeys() + { + $this->GivenDFRObjectsInDB(<<GetCleanupSummary($aClasses); + + $aExpected = [ + 'DFRC1' => ['iDeleteCount' => 5], + 'DFRC2' => ['iDeleteCount' => 1], + 'DFRC3' => ['iDeleteCount' => 3], + 'DFRC4' => ['iDeleteCount' => 1], + ]; + + $this->AssertSummaryEquals($aExpected, $aRes); + + } + + private function AssertSummaryEquals(array $aExpected, array $aActual, string $sMessage = '') + { + foreach ($aExpected as $sClass => $aExpectedCounts) { + $oExpectedCleanupSummaryEntity = new DataCleanupSummaryEntity($sClass); + foreach ($aExpectedCounts as $sCount => $iExpectedValue) { + $oExpectedCleanupSummaryEntity->$sCount = $iExpectedValue; + } + + $this->assertEquals($oExpectedCleanupSummaryEntity, $aActual[$sClass], $sMessage); + } + } +} 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 index a338e1fbc5..f69aa1f314 100644 --- 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 @@ -71,6 +71,15 @@ DEL_AUTO all + + extkey2_id + + + true + DFRToRemove + DEL_AUTO + all + @@ -123,6 +132,15 @@ DEL_AUTO all + + circular_id + + + true + DFRCircularRefs + DEL_AUTO + all + @@ -145,11 +163,11 @@ cmdbAbstractObject - + bizmodel,searchable false - dfrtoremoveleaf + dfrcircularrefs @@ -158,14 +176,23 @@ - - desc + + name true all + + extkey_id + + + false + DFRRemovedCollateral + DEL_AUTO + all + @@ -180,13 +207,13 @@ 10 - + 20 - DFRToRemove + cmdbAbstractObject @@ -292,6 +319,357 @@ cmdbAbstractObject + + + bizmodel,searchable + false + dfrleafnottoremove + + + + + + + + + + info + + true + + + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ DFRToRemove +
+ + + bizmodel,searchable + false + dfrtoremoveleaf + + + + + + + + + + desc + + true + + + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ DFRToRemove +
+ + + + bizmodel,searchable + true + dfrc0 + + + + + + + + + + name + + true + + + all + + + + + + + + + + +
+ + + 10 + + +
+
+ cmdbAbstractObject +
+ + + bizmodel,searchable + false + dfrc1 + + + + + + + + + + name + + true + + + all + + + + + + + + + + +
+ + + 10 + + +
+
+ DFRC0 +
+ + + bizmodel,searchable + false + dfrc2 + + + + + + + + + + name + + true + + + all + + + extkey1_id + + + false + DFRC0 + DEL_AUTO + all + + + extkey2_id + + + false + DFRC0 + DEL_AUTO + all + + + extkey3_id + + + false + DFRC3 + DEL_AUTO + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ cmdbAbstractObject +
+ + + bizmodel,searchable + false + dfrc3 + + + + + + + + + + name + + true + + + all + + + extkey1_id + + + false + DFRC0 + DEL_AUTO + all + + + extkey2_id + + + true + DFRC4 + DEL_AUTO + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ cmdbAbstractObject +
+ + + bizmodel,searchable + false + dfrc4 + + + + + + + + + + name + + true + + + all + + + extkey1_id + + + false + DFRC2 + DEL_AUTO + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ cmdbAbstractObject +
+