Compare commits

..

18 Commits

Author SHA1 Message Date
Eric Espie
588d50c86b N°9639 - wrong field use 2026-06-30 09:44:47 +02:00
Eric Espie
8c80d3a616 N°9639 - cleanup 2026-06-30 09:44:47 +02:00
Eric Espie
31581f8bd6 N°9639 - Enhance DeletionPlanEntity and StaticDeletionPlan with temporary table management and query handling
# Conflicts:
#	datamodels/2.x/combodo-data-feature-removal/src/Service/StaticDeletionPlan.php
2026-06-30 09:44:42 +02:00
Eric Espie
7ddc090514 N°9639 - Remove unused deletion plan from DataFeatureRemovalController 2026-06-30 09:36:29 +02:00
Eric Espie
3da34dabd8 N°9639 - Clarify comment on deletion plan to indicate prevention of infinite loops 2026-06-30 09:16:06 +02:00
Eric Espié
fe32ebcbca Update tests/php-unit-tests/unitary-tests/datamodels/2.x/combodo-data-feature-removal/simulate-audit-from-setup.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-30 09:14:54 +02:00
Eric Espie
a1dcd0300c N°9639 - Refactor variable name for clarity in hierarchical key update process 2026-06-30 09:13:03 +02:00
Eric Espie
b73960a571 N°9639 - Refactor DeletionPlanItem to remove unused queries and simplify constructor 2026-06-29 11:46:08 +02:00
Eric Espie
357ae223fd N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup 2026-06-26 13:50:09 +02:00
Eric Espie
e5e91e8135 N°9639 - Add filtering methods to DeletionPlan classes for improved data cleanup 2026-06-26 13:40:00 +02:00
Eric Espie
87dcbf7b53 N°9639 - Enhance deletion logic and add circular reference handling in cleanup process 2026-06-26 12:05:12 +02:00
Eric Espie
51d5692f81 N°9639 - Refactor SQL queries to use backticks for table and field names 2026-06-26 10:39:54 +02:00
Eric Espie
dc3f6a9f72 N°9639 - Refactor data feature removal logic and update database queries for consistency 2026-06-25 17:20:17 +02:00
Eric Espie
4d2da15c96 N°9639 - Add messages to tests 2026-06-25 13:44:02 +02:00
Eric Espie
63879a63cc N°9639 - Refactor deletion plan methods to use class names instead of table names and add TotalCount method 2026-06-25 13:30:50 +02:00
Eric Espie
bcb42fdc81 N°9639 - Refactor deletion plan handling with new entity classes for improved structure and clarity 2026-06-25 11:17:51 +02:00
Eric Espie
fc9df64eea N°9639 - Fix tests 2026-06-25 08:34:55 +02:00
Eric Espie
41d4f04375 N°9639 - Uninstallation Analysis better count method 2026-06-25 08:33:40 +02:00
16 changed files with 697 additions and 608 deletions

View File

@@ -582,26 +582,16 @@ class UserRightsProfile extends UserRightsAddOnAPI
*/
public function ListProfiles($oUser)
{
if (count($oUser->ListChanges()) === 0) { // backward compatibility
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
} else {
$aRet = [];
$oProfilesSet = $oUser->Get('profile_list');
foreach ($oProfilesSet as $oUserProfile) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
$aRet = [];
$oSearch = new DBObjectSearch('URP_UserProfile');
$oSearch->AllowAllData();
$oSearch->NoContextParameters();
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
$oProfiles = new DBObjectSet($oSearch);
while ($oUserProfile = $oProfiles->Fetch()) {
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
}
return $aRet;
}
public function GetSelectFilter($oUser, $sClass, $aSettings = [])
@@ -715,23 +705,26 @@ class UserRightsProfile extends UserRightsAddOnAPI
protected function GetUserActionGrant($oUser, $sClass, $iActionCode)
{
$this->LoadCache();
if (count($oUser->ListChanges()) === 0) {
// load and cache permissions for the current user on the given class
if (isset($this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
}
// load and cache permissions for the current user on the given class
//
$iUser = $oUser->GetKey();
if (isset($this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode])) {
$aTest = $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode];
if (is_array($aTest)) {
return $aTest;
}
}
$sAction = self::$m_aActionCodes[$iActionCode];
$bStatus = null;
$aProfileList = $this->GetProfileList($oUser);
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
// Call the API of UserRights because it caches the list for us
foreach ($aProfileList as $iProfile => $oProfile) {
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
$bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -749,9 +742,7 @@ class UserRightsProfile extends UserRightsAddOnAPI
$aRes = [
'permission' => $iPermission,
];
if (count($oUser->ListChanges()) === 0) {
$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode] = $aRes;
}
$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes;
return $aRes;
}
@@ -833,14 +824,18 @@ class UserRightsProfile extends UserRightsAddOnAPI
{
$this->LoadCache();
// Note: this code is VERY close to the code of IsActionAllowed()
$iUser = $oUser->GetKey();
$aProfileList = $this->GetProfileList($oUser);
// Cache user's profiles
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
// Note: The object set is ignored because it was interesting to optimize for huge data sets
// and acceptable to consider only the root class of the object set
$bStatus = null;
// Call the API of UserRights because it caches the list for us
foreach ($aProfileList as $iProfile => $oProfile) {
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
$bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode);
if (!is_null($bGrant)) {
if ($bGrant) {
@@ -898,25 +893,6 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
return $bHasSharing;
}
/**
* @param \User $oUser
*
* @return array
* @throws \Exception
*/
public function GetProfileList(User $oUser): array
{
if (count($oUser->ListChanges()) === 0) { // if user is already in db and not changed
$iUser = $oUser->GetKey();
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
$aProfiles = UserRights::ListProfiles($oUser);
$this->aUsersProfilesList[$iUser] = $aProfiles;
}
return $this->aUsersProfilesList[$iUser];
}
return UserRights::ListProfiles($oUser);
}
}
UserRights::SelectModule('UserRightsProfile');

View File

@@ -5761,37 +5761,36 @@ abstract class MetaModel
self::$m_sEnvironment = $sEnvironment;
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
try {
if (!defined('MODULESROOT')) {
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
// $config can be either a filename, or a Configuration object (volatile!)
if ($config instanceof Config) {
self::LoadConfig($config, $bAllowCache);
} else {
self::LoadConfig(new Config($config), $bAllowCache);
}
if ($bModelOnly) {
return;
}
}
if ($bModelOnly) {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
return;
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
}
ExpressionCache::Warmup();
} finally {
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
CMDBSource::SelectDB(self::$m_sDBName);
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
$oPHPClass::OnMetaModelStarted();
}
ExpressionCache::Warmup();
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
EventService::InitService();
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
}
/**

View File

@@ -1530,37 +1530,6 @@ class UserRights
}
}
/**
* @param User $oUser
* @param array $aExcludedProfilesId Administrator by default, but can also be other proofiles depending on needs (e.g. power portal user or REST profile)
* @return bool
* @throws ArchivedObjectException
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws MySQLException
*/
public static function IsUserReadOnly(User $oUser, array $aExcludedProfilesId = [1]): bool
{
$oUserProfiles = $oUser->Get('profile_list');
$oUserRights = UserRights::GetModuleInstance();
while ($oUserProfile = $oUserProfiles->Fetch()) {
$iProfileId = $oUserProfile->Get('profileid');
if (in_array($iProfileId, $aExcludedProfilesId)) {
return false;
}
foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) {
foreach (['w', 'bw', 'd', 'bd'] as $sWriteActionCode) {
$bIsGranted = $oUserRights->GetProfileActionGrant($iProfileId, $sClass, $sWriteActionCode);
if ($bIsGranted === true) {
return false;
}
}
}
}
return true;
}
/**
* @param string $sClass
* @param int $iActionCode see UR_ACTION_* constants

View File

@@ -7,22 +7,31 @@
namespace Combodo\iTop\DataFeatureRemoval\Entity;
use MetaModel;
class DeletionPlanEntity
{
public readonly DeletionPlanItem $oDelete;
public readonly DeletionPlanItem $oUpdate;
public readonly DeletionPlanItem $oIssue;
public array $aQueriesForTempTable;
public array $aDependsOnTempTable;
public string $sClass;
/**
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oDelete
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oUpdate
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oIssue
*/
public function __construct()
public function __construct(string $sClass)
{
$this->sClass = $sClass;
$this->oDelete = $oDelete ?? new DeletionPlanItem();
$this->oUpdate = $oUpdate ?? new DeletionPlanItem();
$this->oIssue = $oIssue ?? new DeletionPlanItem();
$this->aQueriesForTempTable = [];
$this->aDependsOnTempTable = [];
}
public function TotalCount(): int
@@ -34,4 +43,20 @@ class DeletionPlanEntity
{
$this->oUpdate->FilterBy($this->oDelete);
}
public function AddQueryForTempTable(string $sQuery, ?string $sTempTableName = null): void
{
if (!in_array($sQuery, $this->aQueriesForTempTable)) {
$this->aQueriesForTempTable[] = $sQuery;
if (!is_null($sTempTableName) && !in_array($sTempTableName, $this->aDependsOnTempTable)) {
$this->aDependsOnTempTable[] = $sTempTableName;
}
}
}
public static function GetTempTableName(string $sClass): string
{
$sRootClass = MetaModel::GetRootClass($sClass);
return 'temp_'.MetaModel::DBGetTable($sRootClass);
}
}

View File

@@ -7,6 +7,7 @@
namespace Combodo\iTop\DataFeatureRemoval\Service;
use AttributeExternalKey;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanEntity;
@@ -48,6 +49,70 @@ class StaticDeletionPlan
return $aSummary;
}
public function GetTempTableDefinitions(array $aClasses): array
{
$aTempTables = [];
$aDeletionPlan = $this->GetStaticDeletionPlan($aClasses ?? []);
foreach ($aDeletionPlan as $sClass => $oDeletionPlanEntity) {
$sTempTableName = DeletionPlanEntity::GetTempTableName($sClass);
$aTempTables[$sTempTableName]['name'] = $sTempTableName;
$aTempTables[$sTempTableName]['queries'] = array_unique(array_merge($aTempTables[$sTempTableName]['queries'] ?? [], $oDeletionPlanEntity->aQueriesForTempTable));
$aTempTables[$sTempTableName]['depends_on'] = array_unique(array_merge($aTempTables[$sTempTableName]['depends_on'] ?? [], $oDeletionPlanEntity->aDependsOnTempTable));
}
usort($aTempTables, function ($a, $b) use ($aTempTables) {
if (empty($a['depends_on']) && empty($b['depends_on'])) {
// Both initial classes
return 0;
}
return $this->CompareEntries($a, $b, $aTempTables);
});
$aTableDefinitions = [];
foreach ($aTempTables as $aTempTable) {
if (count($aTempTable['queries']) > 0) {
$TempTableSelect = implode("\nUNION\n", $aTempTable['queries']);
$sTempTableName = $aTempTable['name'];
$aTableDefinitions[$sTempTableName] = [
"DROP TEMPORARY TABLE IF EXISTS `$sTempTableName`",
"CREATE TEMPORARY TABLE `$sTempTableName` ($TempTableSelect)",
];
}
}
return $aTableDefinitions;
}
/**
* tells if name1 depends on name2
* @param string $sName1
* @param string $sName2
* @param array $aAllTables
*
* @return bool
*/
private function DependsOn(string $sName1, string $sName2, array $aAllTables): bool
{
$aDependsOn = $aAllTables[$sName1]['depends_on'] ?? [];
if (in_array($sName2, $aDependsOn)) {
return true;
}
// Search on step further
foreach ($aAllTables[$sName1]['depends_on'] as $sParent) {
if ($this->DependsOn($sParent, $sName2, $aAllTables)) {
return true;
}
}
return false;
}
public function CompareEntries(array $aTable1, array $aTable2, array $aAllTables): int
{
return $this->DependsOn($aTable1['name'], $aTable2['name'], $aAllTables) ? 1 : -1;
}
/**
* @param array $aClasses Classes to clean entirely
*
@@ -59,8 +124,9 @@ class StaticDeletionPlan
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
$oDeletionPlanEntity = new DeletionPlanEntity();
$oDeletionPlanEntity = new DeletionPlanEntity($sClass);
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
$oDeletionPlanEntity->AddQueryForTempTable($this->GetQueryForInitialClass($sClass));
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
$this->DeletionPlanForReferencingClasses($sClass);
@@ -75,7 +141,7 @@ class StaticDeletionPlan
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
if (!isset($this->aDeletionPlan[$sRemoteClass])) {
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity();
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity($sRemoteClass);
}
$oDeletionPlanEntity = $this->aDeletionPlan[$sRemoteClass];
/** @var \AttributeExternalKey $oExtKeyAttDef */
@@ -96,7 +162,7 @@ class StaticDeletionPlan
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
// Issue, do not recurse
$oDeletionPlanItem = new DeletionPlanItem(aIds: $aRemoteIdsToRemove);
$oDeletionPlanItem = new DeletionPlanItem($aRemoteIdsToRemove);
$oDeletionPlanEntity->oIssue->Merge($oDeletionPlanItem);
continue;
}
@@ -106,14 +172,16 @@ class StaticDeletionPlan
$sTargetIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]->oDelete->aIds);
$oUpdateItem = $this->UpdateHierarchicalExtKey($sRemoteClass, $sExtKeyAttCode, $sTargetIdsToRemove);
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
// Delete current entry an recurse !
}
// 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);
}
[$sQueryForTempTable, $sDependsOnTempTable] = $this->GetQueryForExtKey($sRemoteClass, $oExtKeyAttDef);
$oDeletionPlanEntity->AddQueryForTempTable($sQueryForTempTable, $sDependsOnTempTable);
$oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove));
// Infinite loops do not occurs due to the datamodel structure
$this->DeletionPlanForReferencingClasses($sRemoteClass);
}
}
}
@@ -145,12 +213,12 @@ class StaticDeletionPlan
*/
public function UpdateHierarchicalExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
{
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sRemoteClass, $sExtKeyAttCode);
$sSQL = <<<SQL
SELECT `$sDBKey`
FROM `$sDBTable` AS `updated`
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBKey`
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBField`
WHERE `removed`.`$sDBKey` IN ($sIdsToRemoveInTargetClass)
SQL;
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
@@ -172,10 +240,10 @@ SQL;
if (\utils::IsNullOrEmptyString($sIdsToRemoveInTargetClass)) {
return [];
}
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sRemoteClass, $sExtKeyAttCode);
$sSQL = "SELECT `$sDBKey` AS id FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
return CMDBSource::QueryToCol($sSQL, $sDBKey);
return CMDBSource::QueryToCol($sSQL, 'id');
}
/**
@@ -187,9 +255,9 @@ SQL;
*/
public function GetInitialClassDeletionPlan(string $sClass): DeletionPlanItem
{
$sTable = MetaModel::DBGetTable($sClass);
$sDBKey = MetaModel::DBGetKey($sClass);
$sSQL = "SELECT `$sDBKey` FROM `$sTable`";
[$sDBTable, $sDBKey] = $this->GetDBInfo($sClass);
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable`";
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
return new DeletionPlanItem($aIds);
@@ -205,15 +273,47 @@ SQL;
* @throws \CoreException
* @throws \Exception
*/
public function GetDBInfoForAttcode(string $sClass, string $sExtKeyAttCode): array
public function GetDBInfo(string $sClass, ?string $sExtKeyAttCode = null): 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];
if (!is_null($sExtKeyAttCode)) {
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode);
$sDBTable = MetaModel::DBGetTable($sOriginClass);
$sDBKey = MetaModel::DBGetKey($sOriginClass);
$oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode);
// External key is on a single DB column
$sDBField = array_keys($oAttDef->GetSQLColumns())[0];
return [$sDBTable, $sDBKey, $sDBField];
}
$sDBTable = MetaModel::DBGetTable($sClass);
$sDBKey = MetaModel::DBGetKey($sClass);
return [$sDBTable, $sDBField, $sDBKey];
return [$sDBTable, $sDBKey];
}
private function GetQueryForInitialClass(mixed $sClass): string
{
[$sDBTable, $sDBKey] = $this->GetDBInfo($sClass);
return "SELECT `$sDBKey` AS id FROM `$sDBTable`";
}
private function GetQueryForExtKey(string $sClass, AttributeExternalKey $oExtKeyAttDef)
{
$sExtKeyAttCode = $oExtKeyAttDef->GetCode();
[$sDBTable, $sDBKey, $sDBField] = $this->GetDBInfo($sClass, $sExtKeyAttCode);
$sTargetClass = $oExtKeyAttDef->GetTargetClass();
$sTempTable = DeletionPlanEntity::GetTempTableName($sTargetClass);
$sQuery = <<<SQL
SELECT `$sDBTable`.`$sDBKey` AS id FROM `$sDBTable`
INNER JOIN `$sTempTable` ON `$sTempTable`.`id` = `$sDBTable`.`$sDBField`
SQL;
return [$sQuery, $sTempTable];
}
}

View File

@@ -85,13 +85,6 @@
<class id="Attachment"/>
</classes>
</group>
<group id="Ticket" _delta="define">
<classes>
<class id="Ticket"/>
<class id="WorkOrder"/>
<class id="Attachment"/>
</classes>
</group>
<group id="Portal" _delta="define">
<classes>
<class id="lnkFunctionalCIToTicket"/>
@@ -212,60 +205,6 @@
</group>
</groups>
<profiles>
<profile id="5500" _delta="define">
<name>Configuration ReadOnly</name>
<description>This read-only profile allows to see CIs objects.</description>
<groups>
<group id="Configuration">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5501" _delta="define">
<name>Ticket ReadOnly</name>
<description>This read-only profile allows to see Ticket objects.</description>
<groups>
<group id="Ticket">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="5502" _delta="define">
<name>Service Catalog ReadOnly</name>
<description>This read-only profile allows to see Service Catalog objects.</description>
<groups>
<group id="Service">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
<group id="General">
<actions>
<action id="action:read">allow</action>
<action id="action:bulk read">allow</action>
</actions>
</group>
</groups>
</profile>
<profile id="117" _delta="define">
<name>SuperUser</name>
<description>This profile allows all actions which are not Administrator restricted.</description>

View File

@@ -4,64 +4,80 @@
<contactid>9</contactid>
<email>jules.vernes@it.com</email>
<org_id_friendlyname>IT Department</org_id_friendlyname>
<login>SupportAgent</login>
<login>Agent</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[SupportAgent]]></password>
<profile_list>
<Set>
<URP_UserProfile id="5">
<profileid>5</profileid>
<profile>Support Agent</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
</Set>
</allowed_org_list>
<password><![CDATA[R6rQ;p]JT*FA$aaP^4]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="5">
<userid>4</userid>
<userlogin>SupportAgent</userlogin>
<profileid>SELECT URP_Profiles WHERE id='5'</profileid>
<profile>Support Agent</profile>
<reason></reason>
</URP_UserProfile>
<UserLocal alias="UserLocal" id="5">
<contactid>26</contactid>
<contactid_friendlyname>Jean Ferrat</contactid_friendlyname>
<org_id_friendlyname>IT Department</org_id_friendlyname>
<login>ConfigManager</login>
<login>config</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[$W[ConfigManager]]></password>
<profile_list>
<Set>
<URP_UserProfile id="6">
<profileid>3</profileid>
<profile>Configuration Manager</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
</Set>
</allowed_org_list>
<password><![CDATA[$W[:"7+Gf"Y\sd8#E~]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="6">
<userid>5</userid>
<userlogin>ConfigManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='3'</profileid>
<profile>Configuration Manager</profile>
<reason></reason>
</URP_UserProfile>
<UserLocal alias="UserLocal" id="2">
<contactid>15</contactid>
<email>agatha.christie@demo.com</email>
<org_id_friendlyname>Sales Department</org_id_friendlyname>
<login>PortalUser</login>
<login>Portal</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[PortalUser]]></password>
<profile_list>
<Set>
<URP_UserProfile id="2">
<profileid>2</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
<URP_UserOrg id="2">
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
</Set>
</allowed_org_list>
<password><![CDATA[-Why]KDdMJvkuB8#e]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-04-17</password_renewed_date>
</UserLocal>
<URP_UserProfile id="2">
<userid>2</userid>
<userlogin>PortalUser</userlogin>
<profileid>SELECT URP_Profiles WHERE id='2'</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserOrg id="2">
<userid>2</userid>
<userlogin>PortalUser</userlogin>
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
<UserLocal alias="UserLocal" id="3">
<contactid>6</contactid>
<email>claude.monet@demo.com</email>
@@ -69,48 +85,32 @@
<login>SalesManager</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[SalesManager]]></password>
<profile_list>
<Set>
<URP_UserProfile id="4">
<profileid>12</profileid>
<profile>Portal power user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserProfile id="3">
<profileid>2</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
</Set>
</profile_list>
<allowed_org_list>
<Set>
<URP_UserOrg id="1">
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
</Set>
</allowed_org_list>
<password><![CDATA[D%r7hoZ})5*hvq5`{Q]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-06-04</password_renewed_date>
</UserLocal>
<URP_UserProfile id="4">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='12'</profileid>
<profile>Portal power user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserProfile id="3">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<profileid>SELECT URP_Profiles WHERE id='2'</profileid>
<profile>Portal user</profile>
<reason></reason>
</URP_UserProfile>
<URP_UserOrg id="1">
<userid>3</userid>
<userlogin>SalesManager</userlogin>
<allowed_org_id>6</allowed_org_id>
<allowed_org_name>Sales Department</allowed_org_name>
<reason></reason>
<allowed_org_id_obsolescence_flag>no</allowed_org_id_obsolescence_flag>
</URP_UserOrg>
<UserLocal alias="UserLocal" id="6">
<contactid>18</contactid>
<email>rene.descartes@demo.com</email>
<org_id_friendlyname>Demo</org_id_friendlyname>
<login>SuperUser</login>
<language>EN US</language>
<status>disabled</status>
<password><![CDATA[SuperUser]]></password>
<expiration>force_expire</expiration>
<password_renewed_date>2026-04-17</password_renewed_date>
</UserLocal>
<URP_UserProfile id="7">
<userid>6</userid>
<userlogin>SuperUser</userlogin>
<profileid>SELECT URP_Profiles WHERE id='117'</profileid>
<profile>Super User</profile>
<reason></reason>
</URP_UserProfile>
</Set>

View File

@@ -2,7 +2,9 @@
namespace Combodo\iTop\Setup\FeatureRemoval;
use ContextTag;
use CoreException;
use Exception;
use IssueLog;
use SetupLog;
use utils;
@@ -29,82 +31,42 @@ class ModelReflectionSerializer
self::$oInstance = $oInstance;
}
public const ERROR_LABEL = "Data consistency check failed: %s";
public function GetModelFromEnvironment(string $sEnv): array
{
IssueLog::Debug(__METHOD__, null, ['env' => $sEnv]);
$sPHPExec = trim(utils::GetConfig()->Get('php_path'));
$aOutput = null;
$sOutput = "";
$iRes = 0;
exec("$sPHPExec --version", $aOutput, $iRes);
if ($iRes != 0) {
$sError = sprintf(self::ERROR_LABEL, "Cannot check CLI/PHP version ($sPHPExec)");
$this->LogSetupError($sError, null, ['env' => $sEnv, 'code' => $iRes, "output" => $aOutput, 'php_path' => $sPHPExec]);
throw new CoreException($sError);
}
$this->CheckCliPhpVersionFromOutput(PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, $sPHPExec, $aOutput);
$aOutput = null;
$iRes = 0;
//preliminary check
$sEnvDir = APPROOT."env-$sEnv";
if (! is_dir($sEnvDir)) {
$sMsg = sprintf(self::ERROR_LABEL, "Missing environment ($sEnvDir)");
$this->LogSetupError($sMsg);
throw new CoreException($sMsg);
}
$sConfigFile = APPROOT."conf/$sEnv/config-itop.php";
if (! is_file($sConfigFile)) {
$sMsg = sprintf(self::ERROR_LABEL, "Missing configuration ($sConfigFile)");
$this->LogSetupError($sMsg);
throw new CoreException($sMsg);
}
$sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv));
exec($sCommandLine, $aOutput, $iRes);
exec($sCommandLine, $sOutput, $iRes);
if ($iRes != 0) {
$sError = $aOutput[0] ?? 'Invalid output when serializing model';
$this->LogSetupError(sprintf(self::ERROR_LABEL, '(cli error) '.$sError), null, ['env' => $sEnv, 'code' => $iRes, "output" => $aOutput, 'cmd' => $sCommandLine]);
throw new CoreException(sprintf(self::ERROR_LABEL, $sError));
$this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput, 'cmd' => $sCommandLine]);
throw new CoreException("Cannot get classes from env ".$sEnv);
}
$aClasses = json_decode($aOutput[0] ?? null, true);
$aClasses = json_decode($sOutput[0] ?? null, true);
if (false === $aClasses) {
$sMsg = sprintf(self::ERROR_LABEL, 'Invalid JSON');
$this->LogSetupError($sMsg, null, ['env' => $sEnv, "output" => $aOutput]);
throw new CoreException($sMsg);
$this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]);
throw new Exception("cannot get classes");
}
if (!is_array($aClasses)) {
$sError = $aOutput[0] ?? 'Invalid json array when serializing model';
$this->LogSetupError(sprintf(self::ERROR_LABEL, '(JSON output not an array) '.$sError), null, ['env' => $sEnv, "classes" => $aClasses, "output" => $aOutput]);
throw new CoreException(sprintf(self::ERROR_LABEL, $sError));
$this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]);
throw new Exception("cannot get classes from $sEnv");
}
return $aClasses;
}
public function CheckCliPhpVersionFromOutput(string $sUIPhpVersion, string $sPHPExec, $aOutput): void
{
$sFoundVersion = trim($aOutput[0] ?? "");
if (preg_match('/(\d+\.\d+)(?:\.\d+)?/', $sFoundVersion, $aMatches)) {
$sFoundVersion = $aMatches[1];
}
if ($sFoundVersion != $sUIPhpVersion) {
$sError = sprintf(self::ERROR_LABEL, "Invalid PHP versions (CLI: $sFoundVersion/ UI: $sUIPhpVersion)");
$this->LogSetupError($sError, null, ["output" => $aOutput, 'php_path' => $sPHPExec]);
throw new CoreException($sError);
}
}
//could be shared with others in log APIs ?
private function LogSetupError($sMessage, $sChannel = null, $aContext = []): void
private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void
{
SetupLog::Enable(APPROOT.'log/setup.log');
SetupLog::Error($sMessage, $sChannel, $aContext);
if (ContextTag::Check(ContextTag::TAG_SETUP)) {
SetupLog::Error($sMessage, $sChannel, $aContext);
} else {
IssueLog::Error($sMessage, $sChannel, $aContext);
}
}
}

View File

@@ -21,7 +21,8 @@ $sConfFile = utils::GetConfigFilePath($sEnv);
try {
MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
} catch (\Throwable $e) {
SetupLog::Enable(APPROOT.'log/setup.log');
echo $e->getMessage();
echo $e->getTraceAsString();
\SetupLog::Error(
"Cannot read model from provided environment",
null,
@@ -31,9 +32,7 @@ try {
'stack' => $e->getTraceAsString(),
]
);
//keep first echo to have proper setup feedbacks
echo $e->getMessage();
echo "Cannot read model from provided environment";
exit(1);
}

View File

@@ -41,5 +41,5 @@
</selected_modules>
<selected_extensions type="array">
</selected_extensions>
<use_symbolic_links>off</use_symbolic_links>
<use_symbolic_links>on</use_symbolic_links>
</installation>

View File

@@ -87,6 +87,8 @@ abstract class ItopDataTestCase extends ItopTestCase
*/
public const DEFAULT_TEST_ENVIRONMENT = 'production';
public const USE_TRANSACTION = true;
public const CREATE_TEST_ORG = false;
protected static $aURP_Profiles = [
'Administrator' => 1,
'Portal user' => 2,
@@ -100,15 +102,9 @@ abstract class ItopDataTestCase extends ItopTestCase
'Service Manager' => 10,
'Document author' => 11,
'Portal power user' => 12,
'Business partner user' => 40,
'REST Services User' => 1024,
'Configuration ReadOnly' => 5500,
'Ticket ReadOnly' => 5501,
'Service Catalog ReadOnly' => 5502,
];
public const CREATE_TEST_ORG = false;
/**
* This method is called before the first test of this test class is run (in the current process).
*/
@@ -1467,42 +1463,16 @@ abstract class ItopDataTestCase extends ItopTestCase
]);
}
/**
* @description To avoid adding finalclasses parameters to GivenUserInDB
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param bool $bReturnLogin
*
* @return string|int The unique login
* @throws \Exception
*/
protected function GivenTokenUserInDB(array $aProfiles, bool $bReturnLogin = true): string|int
{
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
$aProfileList = array_map(function ($sProfileId) {
return 'profileid:'.self::$aURP_Profiles[$sProfileId];
}, $aProfiles);
$iUser = $this->GivenObjectInDB('UserToken', [
'login' => $sLogin,
'language' => 'EN US',
'profile_list' => $aProfileList,
]);
return $bReturnLogin ? $sLogin : $iUser;
}
/**
* @param string $sPassword
* @param array $aProfiles Profile names Example: ['Administrator']
* @param string|null $sLogin
* @param string|null $sUserId
* @param bool $bReturnLogin
*
* @return string The unique login
* @throws \Exception
*/
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null, bool $bReturnLogin = true): string
protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null): string
{
if (is_null($sLogin)) {
$sLogin = 'demo_test_'.uniqid(__CLASS__, true);
@@ -1519,7 +1489,7 @@ abstract class ItopDataTestCase extends ItopTestCase
'profile_list' => $aProfileList,
]);
return $bReturnLogin ? $sLogin : $sUserId;
return $sLogin;
}
/**

View File

@@ -29,11 +29,11 @@ namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreCannotSaveObjectException;
use CoreException;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
use DeleteException;
use Dict;
use MetaModel;
use UserLocal;
use UserRights;
@@ -81,65 +81,6 @@ class UserRightsTest extends ItopDataTestCase
return $oUser;
}
public function testIsActionAllowedWithNonInstantiatedUserObject()
{
$oUser = $this->GivenUserWithProfiles('test1', [self::$aURP_Profiles['Configuration Manager']]); // not a readonly profile
$oAdminUser = $this->GivenUserWithProfiles('test2', [self::$aURP_Profiles['Administrator']]);
$oAdminUser->DBInsert();
$_SESSION = [];
UserRights::Login($oAdminUser->Get('login'));
self::assertTrue(UserRights::IsActionAllowed('Server', UR_ACTION_MODIFY, null, $oUser) === UR_ALLOWED_YES);
}
/**
* @param array $aProfileIds
* @param array $aShouldBeAllowedToSeeClass
* @param array $aShouldBeAllowedToEditClass
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \DictExceptionUnknownLanguage
* @throws \MySQLException
* @throws \OQLException
* @dataProvider ReadOnlyProvider
*/
public function testReadOnlyUser(array $aProfileIds, array $aShouldBeAllowedToSeeClass, array $aShouldBeAllowedToEditClass): void
{
$oUser = $this->GivenUserWithProfiles('test1', $aProfileIds);
$oUser->DBInsert();
$_SESSION = [];
UserRights::Login($oUser->Get('login'));
$aClassesToTest = ['FunctionalCI', 'Ticket', 'ServiceFamily'];
foreach ($aClassesToTest as $sClass) {
$bShouldBeAllowedToSee = in_array($sClass, $aShouldBeAllowedToSeeClass);
$bIsAllowedReading = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_READ);
$this->assertSame(
$bShouldBeAllowedToSee,
$bIsAllowedReading,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToSee ? "" : "NOT ")."be allowed to see class $sClass"
);
$bShouldBeAllowedToEdit = in_array($sClass, $aShouldBeAllowedToEditClass);
$bIsAllowedEditing = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY);
$this->assertSame(
$bIsAllowedEditing,
$bShouldBeAllowedToEdit,
"User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToEdit ? "" : "NOT ")."be allowed to edit class $sClass"
);
}
}
protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject
{
$oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class));
@@ -492,7 +433,7 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->GivenUserWithProfiles('test1', [$iProfileId, 2]);
$this->expectException(CoreCannotSaveObjectException::class);
$this->expectExceptionMessage(Dict::Format('Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice', PORTAL_PROFILE_NAME));
$this->expectExceptionMessage('Profile "Portal user" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)');
$oUser->DBInsert();
}
@@ -631,82 +572,4 @@ class UserRightsTest extends ItopDataTestCase
$oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]);
static::assertNull($oUser, 'FindUser should return null when the login is unknown');
}
protected function ReadOnlyProvider(): array
{
return [
'CI' => [
'ProfilesId' => [
5500,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets' => [
'ProfilesId' => [
5501,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Catalog' => [
'ProfilesId' => [
5502,
],
'ShouldBeAllowedToSeeClasses' => [
'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Tickets' => [
'ProfilesId' => [
5500, 5501,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket',
],
'ShouldBeAllowedToEditClasses' => [],
],
'CI and Catalog' => [
'ProfilesId' => [
5500, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog' => [
'ProfilesId' => [
5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
'Tickets and Catalog + profile Ccnfiguration Manager' => [
'ProfilesId' => [
5501, 5502, 3,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => ['FunctionalCI'],
],
'CI, Tickets and Catalog' => [
'ProfilesId' => [
5500, 5501, 5502,
],
'ShouldBeAllowedToSeeClasses' => [
'FunctionalCI', 'Ticket', 'ServiceFamily',
],
'ShouldBeAllowedToEditClasses' => [],
],
];
}
}

View File

@@ -72,12 +72,13 @@ class StaticDeletionPlanTest extends \AbstractCleanup
public function testGetStaticDeletionPlan_DeleteObjRecursively()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_2
EOF);
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRToUpdate (name = DFRToUpdate_1, extkey_id = DFRToRemoveLeaf_1)
create DFRRemovedCollateral (name = DFRRemovedCollateral_1, extkey_id = DFRToRemoveLeaf_1)
create DFRRemovedCollateralCascade (name = DFRRemovedCollateralCascade_1, extkey_id = DFRRemovedCollateral_1)
create DFRRemovedCollateralCascade (name = DFRRemovedCollateralCascade_2, extkey_id = DFRRemovedCollateral_1)
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new StaticDeletionPlan();
@@ -85,7 +86,6 @@ class StaticDeletionPlanTest extends \AbstractCleanup
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);
}
@@ -174,37 +174,95 @@ EOF);
public function testMultipleExtKeys()
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC1 (name = DFRC1_1)
create DFRC1 (name = DFRC1_2)
create DFRC1 (name = DFRC1_3)
create DFRC1 (name = DFRC1_4)
create DFRC1 (name = DFRC1_5)
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC1_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC1_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC1_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC2 (name = DFRC2_1, extkey1_id = DFRC1_4, extkey2_id = DFRC1_5, extkey3_id = DFRC3_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC2_1)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC1' ];
$aClasses = [ 'DFRC01' ];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRC1' => ['iDeleteCount' => 5],
'DFRC2' => ['iDeleteCount' => 1],
'DFRC01' => ['iDeleteCount' => 5],
'DFRC21' => ['iDeleteCount' => 1],
'DFRC3' => ['iDeleteCount' => 3],
'DFRC4' => ['iDeleteCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testMultipleInitialSubClassesFromSameRoot()
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC02 (name = DFRC02_1)
create DFRC02 (name = DFRC02_2)
create DFRC02 (name = DFRC02_3)
create DFRC03 (name = DFRC03_1)
create DFRC03 (name = DFRC03_2)
create DFRC03 (name = DFRC03_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC3 (name = DFRC3_4, extkey1_id = DFRC02_1)
create DFRC3 (name = DFRC3_5, extkey1_id = DFRC02_2)
create DFRC3 (name = DFRC3_6, extkey1_id = DFRC02_3)
create DFRC3 (name = DFRC3_7, extkey1_id = DFRC03_1)
create DFRC3 (name = DFRC3_8, extkey1_id = DFRC03_2)
create DFRC3 (name = DFRC3_9, extkey1_id = DFRC03_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC01', 'DFRC03' ];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aRes = $oService->GetCleanupSummary($aClasses);
echo json_encode($aRes, JSON_PRETTY_PRINT)."\n";
$aExpected = [
'DFRC01' => ['iDeleteCount' => 5],
'DFRC2' => ['iDeleteCount' => 1],
'DFRC21' => ['iDeleteCount' => 1],
'DFRC3' => ['iDeleteCount' => 6],
'DFRC4' => ['iDeleteCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
private function AssertSummaryEquals(array $aExpected, array $aActual, string $sMessage = '')

View File

@@ -0,0 +1,124 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval;
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
class TempTablesTest extends \AbstractCleanup
{
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/data_cleanup_delta.xml';
}
public function testMultipleExtKeys()
{
echo "--------------------------------------------\n".__METHOD__."\n";
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
EOF);
$aClasses = [ 'DFRC01' ];
$oService = new StaticDeletionPlan();
$aTempTables = $oService->GetTempTableDefinitions($aClasses);
echo json_encode($aTempTables, JSON_PRETTY_PRINT)."\n";
self::assertTrue(true);
}
public function testMultipleInitialSubClassesFromSameRoot()
{
echo "--------------------------------------------\n".__METHOD__."\n";
$this->GivenDFRObjectsInDB(<<<EOF
create DFRC01 (name = DFRC01_1)
create DFRC01 (name = DFRC01_2)
create DFRC01 (name = DFRC01_3)
create DFRC01 (name = DFRC01_4)
create DFRC01 (name = DFRC01_5)
create DFRC02 (name = DFRC02_1)
create DFRC02 (name = DFRC02_2)
create DFRC02 (name = DFRC02_3)
create DFRC03 (name = DFRC03_1)
create DFRC03 (name = DFRC03_2)
create DFRC03 (name = DFRC03_3)
create DFRC3 (name = DFRC3_1, extkey1_id = DFRC01_1)
create DFRC3 (name = DFRC3_2, extkey1_id = DFRC01_2)
create DFRC3 (name = DFRC3_3, extkey1_id = DFRC01_3)
create DFRC3 (name = DFRC3_4, extkey1_id = DFRC02_1)
create DFRC3 (name = DFRC3_5, extkey1_id = DFRC02_2)
create DFRC3 (name = DFRC3_6, extkey1_id = DFRC02_3)
create DFRC3 (name = DFRC3_7, extkey1_id = DFRC03_1)
create DFRC3 (name = DFRC3_8, extkey1_id = DFRC03_2)
create DFRC3 (name = DFRC3_9, extkey1_id = DFRC03_3)
create DFRC21 (name = DFRC21_1, extkey1_id = DFRC01_4, extkey2_id = DFRC01_5, extkey3_id = DFRC3_3)
create DFRC22 (name = DFRC22_1, extkey1_id = DFRC03_1)
create DFRC22 (name = DFRC22_2, extkey1_id = DFRC03_1)
create DFRC4 (name = DFRC4_1, extkey1_id = DFRC21_1)
create DFRC4 (name = DFRC4_2, extkey1_id = DFRC22_1)
create DFRC4 (name = DFRC4_3, extkey1_id = DFRC22_2)
update DFRC3 (name = DFRC3_2, extkey2_id = DFRC4_1)
update DFRC3 (name = DFRC3_5, extkey2_id = DFRC4_2)
update DFRC3 (name = DFRC3_6, extkey2_id = DFRC4_3)
EOF);
$aClasses = [ 'DFRC01', 'DFRC03' ];
$oService = new StaticDeletionPlan();
$aTempTables = $oService->GetTempTableDefinitions($aClasses);
echo json_encode($aTempTables, JSON_PRETTY_PRINT)."\n";
$this->GetIds($aTempTables);
$aRes = $oService->GetStaticDeletionPlan($aClasses);
echo "\nDeletionPlan: ".json_encode($aRes, JSON_PRETTY_PRINT)."\n";
self::assertTrue(true);
}
private function GetIds(array $aTempTables): void
{
foreach ($aTempTables as $sDBName => $aQueries) {
foreach ($aQueries as $sQuery) {
CMDBSource::Query($sQuery);
}
$aIds = CMDBSource::QueryToCol("SELECT id FROM `$sDBName`", 'id');
echo "\n$sDBName: ".implode(', ', $aIds)."\n";
}
}
}

View File

@@ -446,11 +446,43 @@
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRC1" _created_in="itop-structure" _delta="define">
<class id="DFRC00" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>true</abstract>
<db_table>dfrc00</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC0</parent>
</class>
<class id="DFRC01" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc1</db_table>
<db_table>dfrc01</db_table>
<naming>
<attributes/>
</naming>
@@ -484,12 +516,92 @@
</items>
</details>
</presentation>
<parent>DFRC0</parent>
<parent>DFRC00</parent>
</class>
<class id="DFRC02" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc02</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC00</parent>
</class>
<class id="DFRC03" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc03</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC00</parent>
</class>
<class id="DFRC2" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<abstract>true</abstract>
<db_table>dfrc2</db_table>
<naming>
<attributes/>
@@ -516,6 +628,41 @@
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRC21" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc21</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="extkey2_id" xsi:type="AttributeExternalKey">
<sql>extkey2_id</sql>
<filter/>
@@ -554,7 +701,42 @@
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
<parent>DFRC2</parent>
</class>
<class id="DFRC22" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrc22</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRC2</parent>
</class>
<class id="DFRC3" _created_in="itop-structure" _delta="define">
<properties>
@@ -643,7 +825,7 @@
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRC2</target_class>
<target_class>DFRC21</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>

View File

@@ -20,87 +20,10 @@ class ModelSerializationTest extends ItopDataTestCase
$this->assertEqualsCanonicalizing(MetaModel::GetClasses(), $aModel);
}
public function testCheckFail()
{
$sOuput = <<<OUTPUT
PHP 7.4.33 (cli) (built: Aug 2 2024 16:22:28) ( NTS )
OUTPUT;
$this->expectException(\CoreException::class);
$this->expectExceptionMessage("Data consistency check failed: Invalid PHP versions (CLI: 7.4/ UI: 6.6)");
ModelReflectionSerializer::GetInstance()->CheckCliPhpVersionFromOutput('6.6', 'sPHPExec', [$sOuput]);
}
public function CheckOKProvider()
{
return [
["7.4 7.2 7.3.33"],
["PHP 7.4.33 (cli) (built: Aug 2 2024 16:22:28) ( NTS )"],
["PHP 7.4.33 PHP 7.33.22"],
["version: 7.4.27 stable"],
];
}
/**
* @dataProvider CheckOKProvider
*/
public function testCheckOK($sOuput)
{
ModelReflectionSerializer::GetInstance()->CheckCliPhpVersionFromOutput('7.4', 'sPHPExec', [$sOuput]);
$this->assertTrue(true);
}
public function testGetModelFromEnvironmentFailure_NoEnvt()
public function testGetModelFromEnvironmentFailure()
{
$this->expectException(\CoreException::class);
$sEnvDir = APPROOT."env-gabuzomeu";
$this->expectExceptionMessage("Data consistency check failed: Missing environment ($sEnvDir)");
$this->expectExceptionMessage("Cannot get classes");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_NoConfiguration()
{
$sEnvDir = APPROOT."env-gabuzomeu";
$this->aFileToClean [] = $sEnvDir;
mkdir($sEnvDir);
$this->expectException(\CoreException::class);
$sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php";
$this->expectExceptionMessage("Data consistency check failed: Missing configuration ($sConfigFile)");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_BrokenConfiguration()
{
$sEnvDir = APPROOT."env-gabuzomeu";
mkdir($sEnvDir);
$this->aFileToClean [] = $sEnvDir;
mkdir(APPROOT."conf/gabuzomeu");
$this->aFileToClean [] = APPROOT."conf/gabuzomeu";
$sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php";
touch($sConfigFile);
file_put_contents($sConfigFile, 'invalid php content...');
$this->expectException(\CoreException::class);
$sError = <<<ERROR
Syntax error in configuration file: file = $sConfigFile, error = <tt>invalid php content...</tt>
ERROR;
$this->expectExceptionMessage("Data consistency check failed: $sError");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu');
}
public function testGetModelFromEnvironmentFailure_ItopInMaintenanceMode()
{
touch(MAINTENANCE_MODE_FILE);
$this->aFileToClean [] = MAINTENANCE_MODE_FILE;
$this->expectException(\CoreException::class);
$sError = <<<ERROR
This application is currently under maintenance.
ERROR;
$this->expectExceptionMessage("Data consistency check failed: $sError");
ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->GetTestEnvironment());
}
}