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 @@ class CMDBSource $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..299c47cfb3 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\DataFeatureRemovalHelper; 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; @@ -168,6 +169,7 @@ class DataFeatureRemovalController extends Controller $oSetupAudit = new SetupAudit($sSourceEnv); $aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit()); DataFeatureRemovalLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]); + $aDeletionPlan = (new StaticDeletionPlan())->GetStaticDeletionPlan($aGetRemovedClasses); $aParams['aClasses'] = $aGetRemovedClasses; 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..28047d8723 --- /dev/null +++ b/datamodels/2.x/combodo-data-feature-removal/src/Service/StaticDeletionPlan.php @@ -0,0 +1,163 @@ + [ + * 'delete' => [ids], + * 'delete_sql' => string, + * 'update_extkey_nullable' => [ids], + * 'update_extkey_nullable_sql' => [sSQL], + * 'update_hierarchical' => [ids], + * 'update_hierarchical_sql' => [sSQL], + * 'issue' => [id], + * ]]; + * + * @throws \CoreException + */ + public function GetStaticDeletionPlan(array $aClasses): array + { + foreach ($aClasses as $sClass) { + [$sDeleteSQL, $aIds] = $this->GetInitialClassDeletionPlan($sClass); + $this->aDeletionPlan[$sClass] = [ + 'delete' => $aIds, + 'delete_sql' => $sDeleteSQL, + ]; + + $this->DeletionPlanForReferencingClasses($sClass); + } + + return $this->aDeletionPlan; + } + + private function DeletionPlanForReferencingClasses(string $sClass): void + { + $sIdsToRemove = implode(', ', $this->aDeletionPlan[$sClass]['delete']); + $aReferencingMe = MetaModel::EnumReferencingClasses($sClass); + foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) { + $sRemoteTable = MetaModel::DBGetTable($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 + [$sUpdateSQL, $aIds] = $this->UpdateExtKeyNullable($sRemoteTable, $sExtKeyAttCode, $sIdsToRemove); + $this->aDeletionPlan[$sRemoteClass]['update_extkey_nullable_sql'][$sExtKeyAttCode] = $sUpdateSQL; + $this->aDeletionPlan[$sRemoteClass]['update_extkey_nullable'] = array_unique(array_merge($this->aDeletionPlan[$sRemoteClass]['update_extkey_nullable'] ?? [], $aIds)); + } else { + // delete + $aRemoteIdsToRemove = $this->GetRemoteIdsForExtKey($sRemoteTable, $sExtKeyAttCode, $sIdsToRemove); + + $iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption(); + if ($iDeletePropagationOption == DEL_MANUAL) { + // Issue, do not recurse + if (count($aRemoteIdsToRemove) > 0) { + $this->aDeletionPlan[$sRemoteClass]['issue'] = array_unique(array_merge($this->aDeletionPlan[$sRemoteClass]['issue'] ?? [], $aRemoteIdsToRemove)); + } + continue; + } + + if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) { + // update hierarchical keys due to row cleanup in the same table + $sIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]['delete']); + [$sUpdateSQL, $aIds] = $this->UpdateHierarchicalExtKey($sRemoteTable, $sExtKeyAttCode, $sIdsToRemove); + $this->aDeletionPlan[$sRemoteClass]['update_hierarchical_sql'][$sExtKeyAttCode] = $sUpdateSQL; + $this->aDeletionPlan[$sRemoteClass]['update_hierarchical'] = array_unique(array_merge($this->aDeletionPlan[$sRemoteClass]['update_hierarchical'] ?? [], $aIds)); + // do not recurse + continue; + } + + // Delete entries in Remote Class + $this->aDeletionPlan[$sRemoteClass]['delete'] = array_unique(array_merge($this->aDeletionPlan[$sRemoteClass]['delete'] ?? [], $aRemoteIdsToRemove)); + $sRemoteIdsToDelete = implode(',', $aRemoteIdsToRemove); + $this->aDeletionPlan[$sRemoteClass]['delete_sql'] = "DELETE FROM $sRemoteTable WHERE id IN ($sRemoteIdsToDelete)"; + + $this->DeletionPlanForReferencingClasses($sRemoteClass); + } + } + } + } + + /** + * @param string $sRemoteTable + * @param string $sExtKeyAttCode + * @param string $sIdsToRemoveInTargetClass + * + * @return array + */ + public function UpdateExtKeyNullable(string $sRemoteTable, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): array + { + $aIds = $this->GetRemoteIdsForExtKey($sRemoteTable, $sExtKeyAttCode, $sIdsToRemoveInTargetClass); + + $sUpdateSQL = << $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..06de56135f 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 @@ -31,6 +31,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/setup/unattended-install/xml_setup/upgrade.xml b/setup/unattended-install/xml_setup/upgrade.xml index 0272bd2551..bef34398af 100644 --- a/setup/unattended-install/xml_setup/upgrade.xml +++ b/setup/unattended-install/xml_setup/upgrade.xml @@ -41,5 +41,5 @@ - off + on 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 @@ if (SetupUtils::IsSessionSetupTokenValid()) { // 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 @@ HTML; $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 @@ class WizardController 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/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 @@ abstract class WizardStep { } - 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..61891dc5d9 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/AbstractCleanup.php @@ -0,0 +1,56 @@ +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) { + } + + [$sChildClass] = explode('_', $sRight, 2); + + $iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sRight, 'extkey_id' => $iLeftId]); + $this->aIdByClass[$sChildClass][] = $iRightId; + $this->aIdByObjectName[$sRight] = $iRightId; + } +} 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 @@ use PHPUnit\Framework\MockObject\MockObject; * @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 @@ class DataCleanupServiceTest extends ItopCustomDatamodelTestCase $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..5afed8bcbc --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/StaticDeletionPlanTest.php @@ -0,0 +1,102 @@ +GivenDFRTreeInDB(<<GetInitialClassDeletionPlan('DFRToRemoveLeaf'); + self::assertCount(2, $aRes[1]); + self::assertEquals($this->aIdByClass['DFRToRemoveLeaf'], $aRes[1]); + $sTable = MetaModel::DBGetTable('DFRToRemoveLeaf'); + $sExpectedSQL = "DELETE FROM $sTable"; + self::assertEquals($sExpectedSQL, $aRes[0]); + } + + public function testUpdateExtKeyNullable() + { + $this->GivenDFRTreeInDB(<<UpdateExtKeyNullable( + $sRemoteTable, + 'extkey_id', + implode(',', $this->aIdByClass['DFRToRemoveLeaf']) + ); + $sUpdateSQL = $aRes[0]; + $aIds = $aRes[1]; + + // THEN + $sExpectedSQLEnd = " IN (".implode(',', $this->aIdByClass['DFRToRemoveLeaf']).")"; + self::assertStringEndsWith($sExpectedSQLEnd, $sUpdateSQL); + + self::assertCount(3, $aIds); + $sIdsToRemoveInTargetClass = implode(',', $this->aIdByClass['DFRToRemoveLeaf']); + $aExpectedIds = $oService->GetRemoteIdsForExtKey($sRemoteTable, 'extkey_id', $sIdsToRemoveInTargetClass); + + self::assertEquals($aExpectedIds, $aIds); + + // var_export($aRes); + // var_export($this->aIdByClass); + } + + /** + * 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 testExecuteCleanup_DeleteOneObjPerClass() + { + $this->GivenDFRTreeInDB(<<GetStaticDeletionPlan($aClasses); + + var_export($aRes); + + var_export($this->aIdByClass); + } +} 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..9a551fd43b 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 + @@ -145,49 +154,6 @@ cmdbAbstractObject - - - bizmodel,searchable - false - dfrtoremoveleaf - - - - - - - - - - desc - - true - - - all - - - - - - - - - - -
- - - 10 - - - 20 - - -
-
- DFRToRemove -
bizmodel,searchable @@ -292,6 +258,92 @@ cmdbAbstractObject + + + bizmodel,searchable + false + dfrleafnottoremove + + + + + + + + + + info + + true + + + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ DFRToRemove +
+ + + bizmodel,searchable + false + dfrtoremoveleaf + + + + + + + + + + desc + + true + + + all + + + + + + + + + + +
+ + + 10 + + + 20 + + +
+
+ DFRToRemove +
diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php index e48d3f2d1e..9ff9c1bc0d 100644 --- a/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php +++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php @@ -59,7 +59,7 @@ foreach ($aAddedExtensions as $iIndex => $sExtensionCode) { } } -$sRemovedExtensions = utils::ReadParam('removed_modules', '', false, 'raw'); +$sRemovedExtensions = utils::ReadParam('removed_modules', 'itop-container-mgmt', false, 'raw'); $aRemovedExtensionsAndModules = []; if (mb_strlen($sRemovedExtensions) > 0) { $aRemovedExtensionsAndModules = explode(',', $sRemovedExtensions);