From f374395d336baeaf2a672d5cb6d4c42d157f8282 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Fri, 29 May 2026 17:11:12 +0200 Subject: [PATCH 01/22] WIP on new user read-only profiles --- .../datamodel.itop-profiles-itil.xml | 51 +++++++ .../unitary-tests/core/UserRightsTest.php | 124 +++++++++++++++++- 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml index c3ce5d71ee..3be74f7a44 100755 --- a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml +++ b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml @@ -85,6 +85,21 @@ + + + + + + + + + + + + + + + @@ -205,6 +220,42 @@ + + ReadOnlyCI + This read-only profile allows to see CIs objects. + + + + allow + allow + + + + + + ReadOnlyTicket + This read-only profile allows to see Ticket objects. + + + + allow + allow + + + + + + ReadOnlyCatalog + This read-only profile allows to see ServiceFamily objects. + + + + allow + allow + + + + SuperUser This profile allows all actions which are not Administrator restricted. diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php index 07e999a841..d0bde71885 100644 --- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php +++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php @@ -34,6 +34,7 @@ use DBObjectSearch; use DBObjectSet; use DeleteException; +use Dict; use MetaModel; use UserLocal; use UserRights; @@ -96,6 +97,127 @@ protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DB return $oUser; } + /** + * @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 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' => [] + ], + ]; + } public function testIsLoggedIn() { $this->assertFalse(UserRights::IsLoggedIn()); @@ -433,7 +555,7 @@ public function testPrivilegedUsersMustHaveBackofficeAccess(int $iProfileId) $oUser = $this->GivenUserWithProfiles('test1', [$iProfileId, 2]); $this->expectException(CoreCannotSaveObjectException::class); - $this->expectExceptionMessage('Profile "Portal user" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)'); + $this->expectExceptionMessage(Dict::Format('Class:User/Error:PrivilegedUserMustHaveAccessToBackOffice', PORTAL_PROFILE_NAME)); $oUser->DBInsert(); } From 5a0343fc80a24c14e06c220a7fada8dc999b8e14 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 3 Jun 2026 15:26:40 +0200 Subject: [PATCH 02/22] Update profiles definition --- .../datamodel.itop-profiles-itil.xml | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml index 3be74f7a44..be5e447334 100755 --- a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml +++ b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml @@ -88,16 +88,8 @@ - - - - - - - - - - + + @@ -221,10 +213,16 @@ - ReadOnlyCI + Configuration ReadOnly This read-only profile allows to see CIs objects. - + + + allow + allow + + + allow allow @@ -233,7 +231,7 @@ - ReadOnlyTicket + Ticket ReadOnly This read-only profile allows to see Ticket objects. @@ -242,13 +240,25 @@ allow + + + allow + allow + + - ReadOnlyCatalog - This read-only profile allows to see ServiceFamily objects. + Service Catalog ReadOnly + This read-only profile allows to see Service Catalog objects. - + + + allow + allow + + + allow allow From 0f290229e8ee55131c31508a51af7b3a90ca2ee1 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Tue, 2 Jun 2026 18:18:27 +0200 Subject: [PATCH 03/22] WIP --- core/userrights.class.inc.php | 4 +- dictionaries/en.dictionary.itop.core.php | 8 + dictionaries/fr.dictionary.itop.core.php | 8 + lib/composer/autoload_classmap.php | 1 + lib/composer/autoload_static.php | 1 + sources/Users/ITopUserQuotaRepository.php | 221 ++++++++++++++++++ .../Users/ITopUserQuotaRepositoryTest.php | 187 +++++++++++++++ 7 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 sources/Users/ITopUserQuotaRepository.php create mode 100644 tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index ff88fbf638..054abb361c 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -1541,8 +1541,8 @@ public static function GetSelectFilter($sClass, $aSettings = []) */ public static function IsActionAllowed($sClass, $iActionCode, $oInstanceSet = null, $oUser = null) { - // When initializing, we need to let everything pass trough - if (!self::CheckLogin()) { + // When initializing, we need to let everything pass through + if (is_null($oUser) && !self::CheckLogin()) { return UR_ALLOWED_YES; } diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index 8b26efc141..fae27bb901 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -216,6 +216,14 @@ 'Core:Context=GUI:Console' => 'Console', 'Core:Context=CRON' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', + + 'Core:GetQuota:Error' => 'Error while getting %1$s quota', + 'Core:ConsoleUsers' => 'console users', + 'Core:DisabledUsers' => 'disabled users', + 'Core:PortalUsers' => 'portal users', + 'Core:BusinessPartnerUser' => 'business partner users', + 'Core:ReadOnlyUsers' => 'read-only users', + 'Core:ApplicationUsers' => 'application users', ]); ////////////////////////////////////////////////////////////////////// diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 25e31bdb7a..dd1f849806 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -161,6 +161,14 @@ 'Core:Context=CRON+' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', 'Core:Context=GUI:Portal+' => 'GUI:Portal', + + 'Core:GetQuota:Error' => 'Erreur lors de la récupération du quota des %1$s', + 'Core:ConsoleUsers' => 'utilisateurs console', + 'Core:DisabledUsers' => 'utilisateurs désactivés', + 'Core:PortalUsers' => 'utilisateurs du portail', + 'Core:BusinessPartnerUser' => 'utilisateurs partenaires business', + 'Core:ReadOnlyUsers' => 'utilisateurs en lecture seule', + 'Core:ApplicationUsers' => 'utilisateurs applicatifs', ]); ////////////////////////////////////////////////////////////////////// diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 626b2db633..d0654b94a9 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -649,6 +649,7 @@ 'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => $baseDir . '/sources/SessionTracker/iSessionHandlerExtension.php', + 'Combodo\\iTop\\Users\\ITopUserQuotaRepository' => $baseDir . '/sources/Users/ITopUserQuotaRepository.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index fe840689d7..06a6d849a4 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -1050,6 +1050,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => __DIR__ . '/../..' . '/sources/SessionTracker/iSessionHandlerExtension.php', + 'Combodo\\iTop\\Users\\ITopUserQuotaRepository' => __DIR__ . '/../..' . '/sources/Users/ITopUserQuotaRepository.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', diff --git a/sources/Users/ITopUserQuotaRepository.php b/sources/Users/ITopUserQuotaRepository.php new file mode 100644 index 0000000000..3736f48bf6 --- /dev/null +++ b/sources/Users/ITopUserQuotaRepository.php @@ -0,0 +1,221 @@ +getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ConsoleUsers'))); + } + + // TODO remove read only users + return $oFilter; + } + + /** + * @throws Exception + */ + public function GetApplicationUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + { + $sOQLApplicationUser = 'SELECT UserToken'; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); + } + catch (Exception $e) { + IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); + } + + return $oFilter; + + } + + /** + * @throws Exception + */ + public function GetDisabledUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + { + $sOQLDisabledUser = " + SELECT User AS u + WHERE u.status = 'disabled' + "; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); + } + catch (Exception $e) { + IssueLog::Error('Core:GetDisabledUsersQuota:Error : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:DisabledUsers'))); + } + + return $oFilter; + } + +private function IsUserReadOnly(User $oUser, string $sClassCategory) +{ + UserRights::Login($oUser->GetName()); + + foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { + $aClassStimuli = MetaModel::EnumStimuli($sClass); + if (count($aClassStimuli) > 0) { + $aStimuli = []; + foreach ($aClassStimuli as $sStimulusCode => $oStimulus) { + if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $oUser)) { + $aStimuli[] = + $oStimulus->GetLabel(); + } + } + $sStimuli = implode(', ', $aStimuli); + } else { + $sStimuli = ''; + } + + if ( + UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) || + $sStimuli != '' + ) { + UserRights::Logoff(); + return false; + } + } + UserRights::Logoff(); + return true; +} + + /** + * @throws DictExceptionMissingString + * @throws CoreException + * @throws Exception + */ + public function GetReadOnlyUsers(): array + { + $aReadOnlyUsers = []; + $oAllUsersFilter = $this->GetAllUsers(); + $aAllUsers = $this->GetUsersFromFilter($oAllUsersFilter); + /** @var User $oUser */ + foreach ($aAllUsers as $oUser) { + $bIsReadOnlyUser = true; + + if (!$this->IsUserReadOnly($oUser, 'bizmodel') || + !$this->IsUserReadOnly($oUser, 'grant_by_profile')) { + $bIsReadOnlyUser = false; + } + + if ($bIsReadOnlyUser) { + $aReadOnlyUsers[] = $oUser; + } + + } + + // TODO remove disabled users + return $aReadOnlyUsers; + } + + /** + * @throws Exception + */ + public function getPortalUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + { + $sOQLPortalUser = ' + SELECT User AS u + JOIN URP_UserProfile AS uup ON uup.userid = u.id + JOIN URP_Profiles AS up ON uup.profileid = up.id + WHERE up.name = \' '.PORTAL_PROFILE_NAME.'\''; + + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); + } + catch (Exception $e) { + IssueLog::Error('combodo-users-quota-slave/GetUsersInQuota : '.$e->getMessage(), 'combodo-users-quota'); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:PortalUsers'))); + } + + // TODO remove read only users + return $oFilter; + } + + /** + * @throws CoreUnexpectedValue + * @throws CoreException + * @throws MySQLException + */ + public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array + { + $aUsers = []; + if (is_null($oFilter)) { + return $aUsers; + } + $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); + while ($oUser = $oSet->fetch()) { + $aUsers[] = $oUser; + } + + return $aUsers; + } + + /** + * @throws Exception + */ + public function GetAllUsers(bool $bAllData = true): DBUnionSearch|DBObjectSearch|DBSearch|null + { + $sOqlUser = 'SELECT User'; + + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOqlUser) : DBObjectSearch::FromOQL($sOqlUser); + } + catch (Exception $e) { + IssueLog::Error('combodo-users-quota-slave/GetUsersNotInQuota : '.$e->getMessage(), 'combodo-users-quota'); + throw new Exception(Dict::S('CombodoUserQuota:Error')); + } + + return $oFilter; + + } + +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php new file mode 100644 index 0000000000..e9108abfa1 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php @@ -0,0 +1,187 @@ +createUsersQuotaDataset(); + self::$bDatasetInitialized = true; + } + + + /** * Creates a deterministic dataset for quota tests. * Users are created only once (idempotent on login). */ + private function createUsersQuotaDataset(): void + { + // Keep names unique and easy to clean up later if needed. + $sPrefix = 'quota_test_'; + + // Create one user per quota "kind". + // NOTE: profile names can vary by iTop distribution; we try common ones. + $this->createUserIfMissing($sPrefix.'console', true, ['Administrator', 'Configuration Administrator']); + $this->createUserIfMissing($sPrefix.'portal', true, ['Portal user', 'Portal User']); + $this->createUserIfMissing($sPrefix.'readonly', true, ['ReadOnlyCI']); + $this->createUserIfMissing($sPrefix.'application', true, ['Service Desk Agent', 'Change Manager', 'Administrator']); + $this->createUserIfMissing($sPrefix.'disabled', false, ['Service Desk Agent', 'Administrator']); + $this->createUserIfMissing($sPrefix.'disabled', false, ['Service Desk Agent', 'Administrator']); + + + + } + + private function createUserIfMissing(string $sLogin, bool $bEnabled, array $aCandidateProfileNames): void + { + if ($this->findUserByLogin($sLogin) !== null) { + return; + } + + $iProfileId = $this->findFirstProfileIdByNames($aCandidateProfileNames); + $this->assertNotNull( + $iProfileId, + sprintf('Could not find any profile among: %s', implode(', ', $aCandidateProfileNames)) + ); + + $oOrg = MetaModel::NewObject('Organization'); + $oOrg->Set('name', 'Quota Test Org'); + $oOrg->DBInsert(); + + $oPerson = MetaModel::NewObject('Person'); + $oPerson->Set('name', strtoupper($sLogin)); + $oPerson->Set('first_name', 'Quota'); + $oPerson->Set('org_id', $oOrg->GetKey()); + $oPerson->Set('email', $sLogin.'@example.invalid'); + $oPerson->DBInsert(); + + $oUser = MetaModel::NewObject('UserLocal'); + $oUser->Set('login', $sLogin); + $oUser->Set('password', 'QuotaTest#123'); + $oUser->Set('contactid', $oPerson->GetKey()); + $oUser->Set('status', $bEnabled ? 'enabled' : 'disabled'); + + $oProfileList = $oUser->Get('profile_list'); + $oLink = MetaModel::NewObject('URP_UserProfile'); + $oLink->Set('profileid', $iProfileId); + $oProfileList->AddItem($oLink); + $oUser->Set('profile_list', $oProfileList); + + $oUser->DBInsert(); + } + + private function findFirstProfileIdByNames(array $aProfileNames): ?int + { + foreach ($aProfileNames as $sProfileName) { + $oSearch = DBObjectSearch::FromOQL('SELECT URP_Profiles WHERE name = :name'); + $oSet = new CMDBObjectSet($oSearch, [], ['name' => $sProfileName]); + $oProfile = $oSet->Fetch(); + if ($oProfile !== false && $oProfile !== null) { + return (int) $oProfile->GetKey(); + } + } + return null; + } + + private function findUserByLogin(string $sLogin): ?User + { + $oSearch = DBObjectSearch::FromOQL('SELECT User WHERE login = :login'); + $oSet = new CMDBObjectSet($oSearch, [], ['login' => $sLogin]); + $oUser = $oSet->Fetch(); + return ($oUser instanceof User) ? $oUser : null; + } + + + public function testNotDuplicateInDifferentQuotas(): void + { + $oITopUserRepository = new ITopUserQuotaRepository(); + + $aQuotaUsers = [ + 'console' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetConsoleUsers()), + 'portal' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetPortalUsers()), + 'disabled' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetDisabledUsers()), + 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), + 'application' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetApplicationUsers()), + ]; + + $aUserToQuotas = []; + foreach ($aQuotaUsers as $sQuota => $aUsers) { + foreach ($aUsers as $oUser) { + $sUserId = (string) $oUser->GetKey(); + $aUserToQuotas[$sUserId][$sQuota] = true; + } + } + + $aDuplicates = []; + foreach ($aUserToQuotas as $sUserId => $aQuotas) { + $aQuotaNames = array_keys($aQuotas); + if (count($aQuotaNames) > 1) { + sort($aQuotaNames); + $aDuplicates[] = sprintf('User #%s appears in: %s', $sUserId, implode(', ', $aQuotaNames)); + } + } + + $this->assertEmpty( + $aDuplicates, + "Some users are counted in multiple quotas:\n- ".implode("\n- ", $aDuplicates) + ); + } + + public function testAllUsersAreInQuota () { + $oITopUserRepository = new ITopUserQuotaRepository(); + + $oConsoleUsersFilter = $oITopUserRepository->GetConsoleUsers(); + $aConsoleUsers = $oITopUserRepository->GetUsersFromFilter($oConsoleUsersFilter); + $oPortalUsersFilter = $oITopUserRepository->GetPortalUsers(); + $aPortalUsers = $oITopUserRepository->GetUsersFromFilter($oPortalUsersFilter); + $oDisabledUsersFilter = $oITopUserRepository->GetDisabledUsers(); + $aDisabledUsers = $oITopUserRepository->GetUsersFromFilter($oDisabledUsersFilter); + $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); + $oApplicationUsersFilter = $oITopUserRepository->GetApplicationUsers(); + $aApplicationUsers = $oITopUserRepository->GetUsersFromFilter($oApplicationUsersFilter); + + $aAllUsersFromQuota = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + + $oAllUsersFilter = $oITopUserRepository->GetAllUsers(); + $aAllUsersFromOQL = $oITopUserRepository->GetUsersFromFilter($oAllUsersFilter); + + $this->assertEmpty(array_merge(array_diff($aAllUsersFromQuota, $aAllUsersFromOQL), array_diff($aAllUsersFromOQL, $aAllUsersFromQuota))); + } + + public function testAllUsersInQuotaAreUsersObjects () + { + $oITopUserRepository = new ITopUserQuotaRepository(); + + $oConsoleUsersFilter = $oITopUserRepository->GetConsoleUsers(); + $aConsoleUsers = $oITopUserRepository->GetUsersFromFilter($oConsoleUsersFilter); + $oPortalUsersFilter = $oITopUserRepository->GetPortalUsers(); + $aPortalUsers = $oITopUserRepository->GetUsersFromFilter($oPortalUsersFilter); + $oDisabledUsersFilter = $oITopUserRepository->GetDisabledUsers(); + $aDisabledUsers = $oITopUserRepository->GetUsersFromFilter($oDisabledUsersFilter); + $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); + $oApplicationUsersFilter = $oITopUserRepository->GetApplicationUsers(); + $aApplicationUsers = $oITopUserRepository->GetUsersFromFilter($oApplicationUsersFilter); + + $aAllQuotaUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + + foreach ($aAllQuotaUsers as $oUser) { + $this->assertInstanceOf(User::class, $oUser); + } + + + } + +} \ No newline at end of file From 6ca0c927e4d186d3187f615ed284175401ca92c0 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 3 Jun 2026 16:04:05 +0200 Subject: [PATCH 04/22] Rollback --- core/userrights.class.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index 054abb361c..ff88fbf638 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -1541,8 +1541,8 @@ public static function GetSelectFilter($sClass, $aSettings = []) */ public static function IsActionAllowed($sClass, $iActionCode, $oInstanceSet = null, $oUser = null) { - // When initializing, we need to let everything pass through - if (is_null($oUser) && !self::CheckLogin()) { + // When initializing, we need to let everything pass trough + if (!self::CheckLogin()) { return UR_ALLOWED_YES; } From 55940cb2f1504a025df47593a2b3fe048dd3bb64 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 3 Jun 2026 17:57:32 +0200 Subject: [PATCH 05/22] WIP improving quota computing --- sources/Users/ITopUserQuotaRepository.php | 102 ++++++++----- .../src/BaseTestCase/ItopDataTestCase.php | 7 +- .../Users/ITopUserQuotaRepositoryTest.php | 143 +++++------------- 3 files changed, 100 insertions(+), 152 deletions(-) diff --git a/sources/Users/ITopUserQuotaRepository.php b/sources/Users/ITopUserQuotaRepository.php index 3736f48bf6..9094e2165a 100644 --- a/sources/Users/ITopUserQuotaRepository.php +++ b/sources/Users/ITopUserQuotaRepository.php @@ -2,18 +2,20 @@ namespace Combodo\iTop\Users; +use ArchivedObjectException; use CoreException; use CoreUnexpectedValue; use DBObjectSearch; use DBObjectSet; -use DBSearch; use DBUnionSearch; use Dict; use DictExceptionMissingString; +use DictExceptionUnknownLanguage; use Exception; use IssueLog; use MetaModel; use MySQLException; +use OQLException; use User; use UserRights; @@ -28,10 +30,14 @@ class ITopUserQuotaRepository * @param bool $bAllData * @param string $sExcludedFinalClasses * - * @return DBObjectSearch|DBUnionSearch|null + * @return array + * @throws CoreException + * @throws CoreUnexpectedValue + * @throws DictExceptionMissingString + * @throws MySQLException * @throws Exception */ - public function GetConsoleUsers(string $sExcludedUsers = '', string $sExcludedProfiles = '', bool $bAllData = true, string $sExcludedFinalClasses = 'UserToken, UserRemoteSaaS'): null|DBObjectSearch|DBUnionSearch + public function GetConsoleUsers(string $sExcludedUsers = '', string $sExcludedProfiles = '', bool $bAllData = true, string $sExcludedFinalClasses = 'UserToken, UserRemoteSaaS'): array { $sOQLInQuotaUser = " SELECT User AS u @@ -52,14 +58,17 @@ public function GetConsoleUsers(string $sExcludedUsers = '', string $sExcludedPr throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ConsoleUsers'))); } - // TODO remove read only users - return $oFilter; + $aConsoleUsers = $this->GetUsersFromFilter($oFilter); + $aPortalUsers = $this->GetPortalUsers(); + $aReadOnlyUsers = $this->GetReadOnlyUsers(); + + return array_diff($aConsoleUsers, $aPortalUsers, $aReadOnlyUsers); } /** * @throws Exception */ - public function GetApplicationUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + public function GetApplicationUsers(bool $bAllData = true): array { $sOQLApplicationUser = 'SELECT UserToken'; try { @@ -70,14 +79,13 @@ public function GetApplicationUsers(bool $bAllData = true): null|DBObjectSearch| throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); } - return $oFilter; - + return $this->GetUsersFromFilter($oFilter); } /** * @throws Exception */ - public function GetDisabledUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + public function GetDisabledUsers(bool $bAllData = true): array { $sOQLDisabledUser = " SELECT User AS u @@ -91,34 +99,46 @@ public function GetDisabledUsers(bool $bAllData = true): null|DBObjectSearch|DBU throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:DisabledUsers'))); } - return $oFilter; + return $this->GetUsersFromFilter($oFilter); } -private function IsUserReadOnly(User $oUser, string $sClassCategory) + /** + * @throws CoreException + * @throws MySQLException + * @throws CoreUnexpectedValue + * @throws OQLException + * @throws ArchivedObjectException + * @throws DictExceptionUnknownLanguage + */ + private function IsUserReadOnly(User $oUser, string $sClassCategory): bool { - UserRights::Login($oUser->GetName()); + if ($oUser->Get('status') == 'disabled') { + return false; + } - foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { - $aClassStimuli = MetaModel::EnumStimuli($sClass); - if (count($aClassStimuli) > 0) { - $aStimuli = []; - foreach ($aClassStimuli as $sStimulusCode => $oStimulus) { - if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $oUser)) { - $aStimuli[] = - $oStimulus->GetLabel(); - } - } - $sStimuli = implode(', ', $aStimuli); - } else { - $sStimuli = ''; + // check if user is a portal user + $oProfileLinks = $oUser->Get('profile_list'); + while ($oLink = $oProfileLinks->Fetch()) { + $iProfileId = $oLink->Get('profileid'); + if (!$iProfileId) { + continue; + } + $oProfile = MetaModel::GetObject('URP_Profiles', $iProfileId, false); + if ($oProfile && $oProfile->Get('name') === PORTAL_PROFILE_NAME) { + return false; } + } + + // login (mandatory to compute rights) + UserRights::Login($oUser->GetName()); + foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { + // no need to check stimulis for now since users can't execute stimulus without UR_ACTION_MODIFY if ( UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) || - $sStimuli != '' + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) ) { UserRights::Logoff(); return false; @@ -136,31 +156,32 @@ private function IsUserReadOnly(User $oUser, string $sClassCategory) public function GetReadOnlyUsers(): array { $aReadOnlyUsers = []; - $oAllUsersFilter = $this->GetAllUsers(); - $aAllUsers = $this->GetUsersFromFilter($oAllUsersFilter); + $aAllUsers = $this->GetAllUsers(); /** @var User $oUser */ foreach ($aAllUsers as $oUser) { $bIsReadOnlyUser = true; - if (!$this->IsUserReadOnly($oUser, 'bizmodel') || !$this->IsUserReadOnly($oUser, 'grant_by_profile')) { $bIsReadOnlyUser = false; } - if ($bIsReadOnlyUser) { $aReadOnlyUsers[] = $oUser; } - } - // TODO remove disabled users - return $aReadOnlyUsers; + // remove portal users + $aPortalUsers = $this->GetPortalUsers(); + $aReadOnlyUsers = array_diff($aReadOnlyUsers, $aPortalUsers); + // remove disabled users + $aDisabledUsers = $this->GetDisabledUsers(); + + return array_diff($aReadOnlyUsers, $aDisabledUsers); } /** * @throws Exception */ - public function getPortalUsers(bool $bAllData = true): null|DBObjectSearch|DBUnionSearch + public function GetPortalUsers(bool $bAllData = true): array { $sOQLPortalUser = ' SELECT User AS u @@ -177,7 +198,7 @@ public function getPortalUsers(bool $bAllData = true): null|DBObjectSearch|DBUni } // TODO remove read only users - return $oFilter; + return $this->GetUsersFromFilter($oFilter); } /** @@ -192,7 +213,7 @@ public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, a return $aUsers; } $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); - while ($oUser = $oSet->fetch()) { + while ($oUser = $oSet->Fetch()) { $aUsers[] = $oUser; } @@ -202,7 +223,7 @@ public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, a /** * @throws Exception */ - public function GetAllUsers(bool $bAllData = true): DBUnionSearch|DBObjectSearch|DBSearch|null + public function GetAllUsers(bool $bAllData = true): array { $sOqlUser = 'SELECT User'; @@ -213,9 +234,8 @@ public function GetAllUsers(bool $bAllData = true): DBUnionSearch|DBObjectSearch IssueLog::Error('combodo-users-quota-slave/GetUsersNotInQuota : '.$e->getMessage(), 'combodo-users-quota'); throw new Exception(Dict::S('CombodoUserQuota:Error')); } - - return $oFilter; - + + return $this->GetUsersFromFilter($oFilter); } } \ No newline at end of file diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 7be4d9d6be..42468ee949 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -87,8 +87,6 @@ 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, @@ -103,8 +101,13 @@ abstract class ItopDataTestCase extends ItopTestCase 'Document author' => 11, 'Portal power user' => 12, '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). */ diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php index e9108abfa1..a0f58ca8e9 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php @@ -2,119 +2,53 @@ namespace Users; -use CMDBObjectSet; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use Combodo\iTop\Users\ITopUserQuotaRepository; -use DBObjectSearch; -use MetaModel; use User; class ITopUserQuotaRepositoryTest extends ItopDataTestCase{ - - private static bool $bDatasetInitialized = false; - protected function setUp(): void { parent::setUp(); + $this->CreateReadOnlyUsers(); - if (self::$bDatasetInitialized) { - return; - } - - $this->createUsersQuotaDataset(); - self::$bDatasetInitialized = true; } - - /** * Creates a deterministic dataset for quota tests. * Users are created only once (idempotent on login). */ - private function createUsersQuotaDataset(): void + private function CreateReadOnlyUsers() { - // Keep names unique and easy to clean up later if needed. - $sPrefix = 'quota_test_'; - - // Create one user per quota "kind". - // NOTE: profile names can vary by iTop distribution; we try common ones. - $this->createUserIfMissing($sPrefix.'console', true, ['Administrator', 'Configuration Administrator']); - $this->createUserIfMissing($sPrefix.'portal', true, ['Portal user', 'Portal User']); - $this->createUserIfMissing($sPrefix.'readonly', true, ['ReadOnlyCI']); - $this->createUserIfMissing($sPrefix.'application', true, ['Service Desk Agent', 'Change Manager', 'Administrator']); - $this->createUserIfMissing($sPrefix.'disabled', false, ['Service Desk Agent', 'Administrator']); - $this->createUserIfMissing($sPrefix.'disabled', false, ['Service Desk Agent', 'Administrator']); - - - + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Service Catalog ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly', 'Service Catalog ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly', 'Service Catalog ReadOnly']); } - private function createUserIfMissing(string $sLogin, bool $bEnabled, array $aCandidateProfileNames): void - { - if ($this->findUserByLogin($sLogin) !== null) { - return; - } - - $iProfileId = $this->findFirstProfileIdByNames($aCandidateProfileNames); - $this->assertNotNull( - $iProfileId, - sprintf('Could not find any profile among: %s', implode(', ', $aCandidateProfileNames)) - ); - - $oOrg = MetaModel::NewObject('Organization'); - $oOrg->Set('name', 'Quota Test Org'); - $oOrg->DBInsert(); - - $oPerson = MetaModel::NewObject('Person'); - $oPerson->Set('name', strtoupper($sLogin)); - $oPerson->Set('first_name', 'Quota'); - $oPerson->Set('org_id', $oOrg->GetKey()); - $oPerson->Set('email', $sLogin.'@example.invalid'); - $oPerson->DBInsert(); - - $oUser = MetaModel::NewObject('UserLocal'); - $oUser->Set('login', $sLogin); - $oUser->Set('password', 'QuotaTest#123'); - $oUser->Set('contactid', $oPerson->GetKey()); - $oUser->Set('status', $bEnabled ? 'enabled' : 'disabled'); - - $oProfileList = $oUser->Get('profile_list'); - $oLink = MetaModel::NewObject('URP_UserProfile'); - $oLink->Set('profileid', $iProfileId); - $oProfileList->AddItem($oLink); - $oUser->Set('profile_list', $oProfileList); - - $oUser->DBInsert(); + private function CreateDisabledUser() { + $sUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager']); + // get user by login + $oUser = \MetaModel::GetObjectByName('User', $sUser); + $oUser->Set('status', 'disabled'); + $oUser->DBUpdate(); } - private function findFirstProfileIdByNames(array $aProfileNames): ?int - { - foreach ($aProfileNames as $sProfileName) { - $oSearch = DBObjectSearch::FromOQL('SELECT URP_Profiles WHERE name = :name'); - $oSet = new CMDBObjectSet($oSearch, [], ['name' => $sProfileName]); - $oProfile = $oSet->Fetch(); - if ($oProfile !== false && $oProfile !== null) { - return (int) $oProfile->GetKey(); - } - } - return null; - } - - private function findUserByLogin(string $sLogin): ?User - { - $oSearch = DBObjectSearch::FromOQL('SELECT User WHERE login = :login'); - $oSet = new CMDBObjectSet($oSearch, [], ['login' => $sLogin]); - $oUser = $oSet->Fetch(); - return ($oUser instanceof User) ? $oUser : null; - } - - + /** + * @throws \CoreUnexpectedValue + * @throws \DictExceptionMissingString + * @throws \CoreException + * @throws \MySQLException + * @throws \Exception + */ public function testNotDuplicateInDifferentQuotas(): void { $oITopUserRepository = new ITopUserQuotaRepository(); $aQuotaUsers = [ - 'console' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetConsoleUsers()), - 'portal' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetPortalUsers()), - 'disabled' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetDisabledUsers()), + 'console' => $oITopUserRepository->GetConsoleUsers(), + 'portal' => $oITopUserRepository->GetPortalUsers(), + 'disabled' => $oITopUserRepository->GetDisabledUsers(), 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), - 'application' => $oITopUserRepository->GetUsersFromFilter($oITopUserRepository->GetApplicationUsers()), + 'application' => $oITopUserRepository->GetApplicationUsers(), ]; $aUserToQuotas = []; @@ -143,20 +77,15 @@ public function testNotDuplicateInDifferentQuotas(): void public function testAllUsersAreInQuota () { $oITopUserRepository = new ITopUserQuotaRepository(); - $oConsoleUsersFilter = $oITopUserRepository->GetConsoleUsers(); - $aConsoleUsers = $oITopUserRepository->GetUsersFromFilter($oConsoleUsersFilter); - $oPortalUsersFilter = $oITopUserRepository->GetPortalUsers(); - $aPortalUsers = $oITopUserRepository->GetUsersFromFilter($oPortalUsersFilter); - $oDisabledUsersFilter = $oITopUserRepository->GetDisabledUsers(); - $aDisabledUsers = $oITopUserRepository->GetUsersFromFilter($oDisabledUsersFilter); + $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); + $aPortalUsers = $oITopUserRepository->GetPortalUsers(); + $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $oApplicationUsersFilter = $oITopUserRepository->GetApplicationUsers(); - $aApplicationUsers = $oITopUserRepository->GetUsersFromFilter($oApplicationUsersFilter); + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); $aAllUsersFromQuota = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); - $oAllUsersFilter = $oITopUserRepository->GetAllUsers(); - $aAllUsersFromOQL = $oITopUserRepository->GetUsersFromFilter($oAllUsersFilter); + $aAllUsersFromOQL = $oITopUserRepository->GetAllUsers(); $this->assertEmpty(array_merge(array_diff($aAllUsersFromQuota, $aAllUsersFromOQL), array_diff($aAllUsersFromOQL, $aAllUsersFromQuota))); } @@ -165,15 +94,11 @@ public function testAllUsersInQuotaAreUsersObjects () { $oITopUserRepository = new ITopUserQuotaRepository(); - $oConsoleUsersFilter = $oITopUserRepository->GetConsoleUsers(); - $aConsoleUsers = $oITopUserRepository->GetUsersFromFilter($oConsoleUsersFilter); - $oPortalUsersFilter = $oITopUserRepository->GetPortalUsers(); - $aPortalUsers = $oITopUserRepository->GetUsersFromFilter($oPortalUsersFilter); - $oDisabledUsersFilter = $oITopUserRepository->GetDisabledUsers(); - $aDisabledUsers = $oITopUserRepository->GetUsersFromFilter($oDisabledUsersFilter); + $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); + $aPortalUsers = $oITopUserRepository->GetPortalUsers(); + $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $oApplicationUsersFilter = $oITopUserRepository->GetApplicationUsers(); - $aApplicationUsers = $oITopUserRepository->GetUsersFromFilter($oApplicationUsersFilter); + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); $aAllQuotaUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); From ac276e8ae272cf71ff4056a7f6c2889ece6eede8 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Mon, 8 Jun 2026 18:41:01 +0200 Subject: [PATCH 06/22] WIP --- dictionaries/en.dictionary.itop.core.php | 2 +- lib/composer/autoload_classmap.php | 2 +- lib/composer/autoload_static.php | 2 +- ...ory.php => ITopUserCountingRepository.php} | 237 +++++++++--------- ...php => ITopUserCountingRepositoryTest.php} | 52 ++-- 5 files changed, 154 insertions(+), 141 deletions(-) rename sources/Users/{ITopUserQuotaRepository.php => ITopUserCountingRepository.php} (62%) rename tests/php-unit-tests/unitary-tests/sources/Users/{ITopUserQuotaRepositoryTest.php => ITopUserCountingRepositoryTest.php} (62%) diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index fae27bb901..178c752bd9 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -217,7 +217,7 @@ 'Core:Context=CRON' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', - 'Core:GetQuota:Error' => 'Error while getting %1$s quota', + 'Core:GetCountingUsers:Error' => 'Error while getting %1$s quota', 'Core:ConsoleUsers' => 'console users', 'Core:DisabledUsers' => 'disabled users', 'Core:PortalUsers' => 'portal users', diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index d0654b94a9..9bc9e59445 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -649,7 +649,7 @@ 'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => $baseDir . '/sources/SessionTracker/iSessionHandlerExtension.php', - 'Combodo\\iTop\\Users\\ITopUserQuotaRepository' => $baseDir . '/sources/Users/ITopUserQuotaRepository.php', + 'Combodo\\iTop\\Users\\ITopUserCountingRepository' => $baseDir . '/sources/Users/ITopUserCountingRepository.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 06a6d849a4..edda3e727c 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -1050,7 +1050,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => __DIR__ . '/../..' . '/sources/SessionTracker/iSessionHandlerExtension.php', - 'Combodo\\iTop\\Users\\ITopUserQuotaRepository' => __DIR__ . '/../..' . '/sources/Users/ITopUserQuotaRepository.php', + 'Combodo\\iTop\\Users\\ITopUserCountingRepository' => __DIR__ . '/../..' . '/sources/Users/ITopUserCountingRepository.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', diff --git a/sources/Users/ITopUserQuotaRepository.php b/sources/Users/ITopUserCountingRepository.php similarity index 62% rename from sources/Users/ITopUserQuotaRepository.php rename to sources/Users/ITopUserCountingRepository.php index 9094e2165a..75bb20c680 100644 --- a/sources/Users/ITopUserQuotaRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -22,13 +22,13 @@ /** * */ -class ITopUserQuotaRepository +class ITopUserCountingRepository { /** - * @param string $sExcludedUsers - * @param string $sExcludedProfiles + * @param array $aExcludedUsers + * @param array $aExcludedProfiles * @param bool $bAllData - * @param string $sExcludedFinalClasses + * @param array $aExcludedFinalClasses * * @return array * @throws CoreException @@ -37,23 +37,26 @@ class ITopUserQuotaRepository * @throws MySQLException * @throws Exception */ - public function GetConsoleUsers(string $sExcludedUsers = '', string $sExcludedProfiles = '', bool $bAllData = true, string $sExcludedFinalClasses = 'UserToken, UserRemoteSaaS'): array + public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProfiles = [], bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array { - $sOQLInQuotaUser = " - SELECT User AS u - WHERE u.status != 'disabled' - AND u.login NOT IN ('$sExcludedUsers') - AND u.finalclass != ' $sExcludedFinalClasses ' - AND id NOT IN ( - SELECT User AS uex - JOIN URP_UserProfile AS uup ON uup.userid = uex.id - JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.name IN ('$sExcludedProfiles')) - "; + $sExcludedUsers = $this->ArrayToOQLStringParameter($aExcludedUsers); + $sExcludedProfiles = $this->ArrayToOQLStringParameter($aExcludedProfiles); + $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); + + $sOQLUserConsole = " + SELECT User AS u + WHERE u.status != 'disabled' + AND u.login NOT IN ($sExcludedUsers) + AND u.finalclass NOT IN ($sExcludedFinalClasses) + AND id NOT IN ( + SELECT User AS uex + JOIN URP_UserProfile AS uup ON uup.userid = uex.id + JOIN URP_Profiles AS up ON uup.profileid = up.id + WHERE up.name IN ($sExcludedProfiles)) + "; try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLInQuotaUser) : DBObjectSearch::FromOQL($sOQLInQuotaUser); - } - catch (Exception $e) { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLUserConsole) : DBObjectSearch::FromOQL($sOQLUserConsole); + } catch (Exception $e) { IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ConsoleUsers'))); } @@ -65,88 +68,59 @@ public function GetConsoleUsers(string $sExcludedUsers = '', string $sExcludedPr return array_diff($aConsoleUsers, $aPortalUsers, $aReadOnlyUsers); } - /** - * @throws Exception - */ - public function GetApplicationUsers(bool $bAllData = true): array + private function ArrayToOQLStringParameter(array $aValues): string { - $sOQLApplicationUser = 'SELECT UserToken'; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); - } - catch (Exception $e) { - IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); + $aQuotedValues = []; + foreach ($aValues as $value) { + $value = trim((string) $value); + if ($value === '') { + continue; + } + $aQuotedValues[] = "'".addslashes($value)."'"; } - return $this->GetUsersFromFilter($oFilter); + return empty($aQuotedValues) ? "''" : implode(', ', $aQuotedValues); } /** - * @throws Exception + * @throws CoreUnexpectedValue + * @throws CoreException + * @throws MySQLException */ - public function GetDisabledUsers(bool $bAllData = true): array + public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array { - $sOQLDisabledUser = " - SELECT User AS u - WHERE u.status = 'disabled' - "; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); + $aUsers = []; + if (is_null($oFilter)) { + return $aUsers; } - catch (Exception $e) { - IssueLog::Error('Core:GetDisabledUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:DisabledUsers'))); + $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); + while ($oUser = $oSet->Fetch()) { + $aUsers[] = $oUser; } - return $this->GetUsersFromFilter($oFilter); + return $aUsers; } /** - * @throws CoreException - * @throws MySQLException - * @throws CoreUnexpectedValue - * @throws OQLException - * @throws ArchivedObjectException - * @throws DictExceptionUnknownLanguage + * @throws Exception */ - private function IsUserReadOnly(User $oUser, string $sClassCategory): bool -{ - if ($oUser->Get('status') == 'disabled') { - return false; - } + public function GetPortalUsers(bool $bAllData = true): array + { + $sOQLPortalUser = " + SELECT User AS u + JOIN URP_UserProfile AS uup ON uup.userid = u.id + JOIN URP_Profiles AS up ON uup.profileid = up.id + WHERE up.id = '2' AND u.status != 'disabled' "; - // check if user is a portal user - $oProfileLinks = $oUser->Get('profile_list'); - while ($oLink = $oProfileLinks->Fetch()) { - $iProfileId = $oLink->Get('profileid'); - if (!$iProfileId) { - continue; - } - $oProfile = MetaModel::GetObject('URP_Profiles', $iProfileId, false); - if ($oProfile && $oProfile->Get('name') === PORTAL_PROFILE_NAME) { - return false; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); + } catch (Exception $e) { + IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); } - } - // login (mandatory to compute rights) - UserRights::Login($oUser->GetName()); - - foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { - // no need to check stimulis for now since users can't execute stimulus without UR_ACTION_MODIFY - if ( - UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) - ) { - UserRights::Logoff(); - return false; - } + return $this->GetUsersFromFilter($oFilter); } - UserRights::Logoff(); - return true; -} /** * @throws DictExceptionMissingString @@ -181,61 +155,98 @@ public function GetReadOnlyUsers(): array /** * @throws Exception */ - public function GetPortalUsers(bool $bAllData = true): array + public function GetAllUsers(bool $bAllData = true): array { - $sOQLPortalUser = ' - SELECT User AS u - JOIN URP_UserProfile AS uup ON uup.userid = u.id - JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.name = \' '.PORTAL_PROFILE_NAME.'\''; + $sOqlUser = 'SELECT User'; try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); - } - catch (Exception $e) { - IssueLog::Error('combodo-users-quota-slave/GetUsersInQuota : '.$e->getMessage(), 'combodo-users-quota'); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:PortalUsers'))); + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOqlUser) : DBObjectSearch::FromOQL($sOqlUser); + } catch (Exception $e) { + IssueLog::Error('combodo-users-quota-slave/GetUsersNotInQuota : '.$e->getMessage(), 'combodo-users-quota'); + throw new Exception(Dict::S('CombodoUserQuota:Error')); } - // TODO remove read only users return $this->GetUsersFromFilter($oFilter); } /** - * @throws CoreUnexpectedValue * @throws CoreException * @throws MySQLException + * @throws CoreUnexpectedValue + * @throws OQLException + * @throws ArchivedObjectException + * @throws DictExceptionUnknownLanguage */ - public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array + private function IsUserReadOnly(User $oUser, string $sClassCategory): bool { - $aUsers = []; - if (is_null($oFilter)) { - return $aUsers; + if ($oUser->Get('status') == 'disabled') { + return false; } - $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); - while ($oUser = $oSet->Fetch()) { - $aUsers[] = $oUser; + + // check if user is a portal user + $oProfileLinks = $oUser->Get('profile_list'); + while ($oLink = $oProfileLinks->Fetch()) { + $iProfileId = $oLink->Get('profileid'); + if (!$iProfileId) { + continue; + } + $oProfile = MetaModel::GetObject('URP_Profiles', $iProfileId, false); + if ($oProfile && $oProfile->Get('name') === PORTAL_PROFILE_NAME) { + return false; + } } - return $aUsers; + // login (mandatory to compute rights) + UserRights::Login($oUser->GetName()); + + foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { + // no need to check stimuli for now since users can't execute stimulus without UR_ACTION_MODIFY + if ( + UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) + ) { + UserRights::Logoff(); + return false; + } + } + UserRights::Logoff(); + return true; } /** * @throws Exception */ - public function GetAllUsers(bool $bAllData = true): array + public function GetDisabledUsers(bool $bAllData = true): array { - $sOqlUser = 'SELECT User'; - + $sOQLDisabledUser = " + SELECT User AS u + WHERE u.status = 'disabled' + "; try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOqlUser) : DBObjectSearch::FromOQL($sOqlUser); - } - catch (Exception $e) { - IssueLog::Error('combodo-users-quota-slave/GetUsersNotInQuota : '.$e->getMessage(), 'combodo-users-quota'); - throw new Exception(Dict::S('CombodoUserQuota:Error')); + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); + } catch (Exception $e) { + IssueLog::Error('Core:GetDisabledUsersQuota:Error : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:DisabledUsers'))); } - + return $this->GetUsersFromFilter($oFilter); } -} \ No newline at end of file + /** + * @throws Exception + */ + public function GetApplicationUsers(bool $bAllData = true): array + { + $sOQLApplicationUser = 'SELECT UserToken'; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); + } catch (Exception $e) { + IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); + } + + return $this->GetUsersFromFilter($oFilter); + } +} diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php similarity index 62% rename from tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php rename to tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index a0f58ca8e9..8ea985757c 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserQuotaRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -3,10 +3,11 @@ namespace Users; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; -use Combodo\iTop\Users\ITopUserQuotaRepository; +use Combodo\iTop\Users\ITopUserCountingRepository; use User; -class ITopUserQuotaRepositoryTest extends ItopDataTestCase{ +class ITopUserCountingRepositoryTest extends ItopDataTestCase +{ protected function setUp(): void { parent::setUp(); @@ -24,7 +25,8 @@ private function CreateReadOnlyUsers() $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly', 'Service Catalog ReadOnly']); } - private function CreateDisabledUser() { + private function CreateDisabledUser() + { $sUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager']); // get user by login $oUser = \MetaModel::GetObjectByName('User', $sUser); @@ -39,11 +41,11 @@ private function CreateDisabledUser() { * @throws \MySQLException * @throws \Exception */ - public function testNotDuplicateInDifferentQuotas(): void + public function testNotDuplicateInDifferentCountsCategories(): void { - $oITopUserRepository = new ITopUserQuotaRepository(); + $oITopUserRepository = new ITopUserCountingRepository(); - $aQuotaUsers = [ + $aCountedUsers = [ 'console' => $oITopUserRepository->GetConsoleUsers(), 'portal' => $oITopUserRepository->GetPortalUsers(), 'disabled' => $oITopUserRepository->GetDisabledUsers(), @@ -51,31 +53,32 @@ public function testNotDuplicateInDifferentQuotas(): void 'application' => $oITopUserRepository->GetApplicationUsers(), ]; - $aUserToQuotas = []; - foreach ($aQuotaUsers as $sQuota => $aUsers) { + $aCountedUserFormated = []; + foreach ($aCountedUsers as $sCountedCategory => $aUsers) { foreach ($aUsers as $oUser) { $sUserId = (string) $oUser->GetKey(); - $aUserToQuotas[$sUserId][$sQuota] = true; + $aCountedUserFormated[$sUserId][$sCountedCategory] = true; } } $aDuplicates = []; - foreach ($aUserToQuotas as $sUserId => $aQuotas) { - $aQuotaNames = array_keys($aQuotas); - if (count($aQuotaNames) > 1) { - sort($aQuotaNames); - $aDuplicates[] = sprintf('User #%s appears in: %s', $sUserId, implode(', ', $aQuotaNames)); + foreach ($aCountedUserFormated as $sUserId => $aCountedCategory) { + $aCountedCategoryName = array_keys($aCountedCategory); + if (count($aCountedCategoryName) > 1) { + sort($aCountedCategoryName); + $aDuplicates[] = sprintf('User #%s appears in: %s', $sUserId, implode(', ', $aCountedCategoryName)); } } $this->assertEmpty( $aDuplicates, - "Some users are counted in multiple quotas:\n- ".implode("\n- ", $aDuplicates) + "Some users are counted in multiple categories:\n- ".implode("\n- ", $aDuplicates) ); } - public function testAllUsersAreInQuota () { - $oITopUserRepository = new ITopUserQuotaRepository(); + public function testAllUsersAreCounted() + { + $oITopUserRepository = new ITopUserCountingRepository(); $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); $aPortalUsers = $oITopUserRepository->GetPortalUsers(); @@ -83,16 +86,16 @@ public function testAllUsersAreInQuota () { $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); - $aAllUsersFromQuota = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); $aAllUsersFromOQL = $oITopUserRepository->GetAllUsers(); - $this->assertEmpty(array_merge(array_diff($aAllUsersFromQuota, $aAllUsersFromOQL), array_diff($aAllUsersFromOQL, $aAllUsersFromQuota))); + $this->assertEmpty(array_merge(array_diff($aAllUsersFromMergedCounts, $aAllUsersFromOQL), array_diff($aAllUsersFromOQL, $aAllUsersFromMergedCounts))); } - public function testAllUsersInQuotaAreUsersObjects () + public function testAllCountedUsersAreUsersObjects() { - $oITopUserRepository = new ITopUserQuotaRepository(); + $oITopUserRepository = new ITopUserCountingRepository(); $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); $aPortalUsers = $oITopUserRepository->GetPortalUsers(); @@ -100,13 +103,12 @@ public function testAllUsersInQuotaAreUsersObjects () $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); - $aAllQuotaUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); - foreach ($aAllQuotaUsers as $oUser) { + foreach ($aCountedUsers as $oUser) { $this->assertInstanceOf(User::class, $oUser); } - } -} \ No newline at end of file +} From 9061f0317b2e4d329c7700e750221ddd4833a580 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Tue, 9 Jun 2026 15:34:34 +0200 Subject: [PATCH 07/22] Improve test method to be able to get login OR id of the created user --- tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 42468ee949..7a58d0394f 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1471,11 +1471,12 @@ protected function GivenUserWithContactInDB(string $sLogin, string $sProfileId, * @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): string + protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $sLogin = null, ?string &$sUserId = null, bool $bReturnLogin = true): string { if (is_null($sLogin)) { $sLogin = 'demo_test_'.uniqid(__CLASS__, true); @@ -1492,7 +1493,7 @@ protected function GivenUserInDB(string $sPassword, array $aProfiles, ?string $s 'profile_list' => $aProfileList, ]); - return $sLogin; + return $bReturnLogin ? $sLogin : $sUserId; } /** From 75780bfc1fa7c683eaf8c94f05b28f9d03f1769b Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Tue, 9 Jun 2026 15:58:41 +0200 Subject: [PATCH 08/22] WIP --- dictionaries/en.dictionary.itop.core.php | 1075 +++++++++-------- dictionaries/fr.dictionary.itop.core.php | 15 +- sources/Users/ITopUserCountingRepository.php | 165 ++- .../src/BaseTestCase/ItopDataTestCase.php | 25 + .../Users/ITopUserCountingRepositoryTest.php | 43 +- 5 files changed, 681 insertions(+), 642 deletions(-) diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index 178c752bd9..b7ecb91796 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -24,125 +24,125 @@ Dict::Add('EN US', 'English', 'English', [ 'Core:DeletedObjectLabel' => '%1s (deleted)', - 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)', + 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)', - 'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.', + 'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.', 'Core:UniquenessDefaultError' => 'Uniqueness rule \'%1$s\' in error', - 'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s', - 'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s', + 'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s', + 'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s', - 'Core:AttributeLinkedSet' => 'Array of objects', + 'Core:AttributeLinkedSet' => 'Array of objects', 'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass', 'Core:AttributeLinkedSetDuplicatesFound' => 'Duplicates in the \'%1$s\' field : %2$s', - 'Core:AttributeDashboard' => 'Dashboard', + 'Core:AttributeDashboard' => 'Dashboard', 'Core:AttributeDashboard+' => '', - 'Core:AttributePhoneNumber' => 'Phone number', + 'Core:AttributePhoneNumber' => 'Phone number', 'Core:AttributePhoneNumber+' => '', - 'Core:AttributeObsolescenceDate' => 'Obsolescence date', + 'Core:AttributeObsolescenceDate' => 'Obsolescence date', 'Core:AttributeObsolescenceDate+' => '', - 'Core:AttributeTagSet' => 'List of tags', - 'Core:AttributeTagSet+' => '', - 'Core:AttributeSet:placeholder' => 'click to add', - 'Core:Placeholder:CannotBeResolved' => '(%1$s : cannot be resolved)', - 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)', - 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)', + 'Core:AttributeTagSet' => 'List of tags', + 'Core:AttributeTagSet+' => '', + 'Core:AttributeSet:placeholder' => 'click to add', + 'Core:Placeholder:CannotBeResolved' => '(%1$s : cannot be resolved)', + 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)', + 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)', 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses' => '%1$s (%2$s from child classes)', - 'Core:AttributeCaseLog' => 'Log', + 'Core:AttributeCaseLog' => 'Log', 'Core:AttributeCaseLog+' => '', - 'Core:AttributeMetaEnum' => 'Computed enum', + 'Core:AttributeMetaEnum' => 'Computed enum', 'Core:AttributeMetaEnum+' => '', - 'Core:AttributeLinkedSetIndirect' => 'Array of objects (N-N)', + 'Core:AttributeLinkedSetIndirect' => 'Array of objects (N-N)', 'Core:AttributeLinkedSetIndirect+' => 'Any kind of objects [subclass] of the same class', - 'Core:AttributeInteger' => 'Integer', + 'Core:AttributeInteger' => 'Integer', 'Core:AttributeInteger+' => 'Numeric value (could be negative)', - 'Core:AttributeDecimal' => 'Decimal', + 'Core:AttributeDecimal' => 'Decimal', 'Core:AttributeDecimal+' => 'Decimal value (could be negative)', - 'Core:AttributeBoolean' => 'Boolean', - 'Core:AttributeBoolean+' => '', + 'Core:AttributeBoolean' => 'Boolean', + 'Core:AttributeBoolean+' => '', 'Core:AttributeBoolean/Value:null' => '', - 'Core:AttributeBoolean/Value:yes' => 'Yes', - 'Core:AttributeBoolean/Value:no' => 'No', + 'Core:AttributeBoolean/Value:yes' => 'Yes', + 'Core:AttributeBoolean/Value:no' => 'No', - 'Core:AttributeArchiveFlag' => 'Archive flag', - 'Core:AttributeArchiveFlag/Value:yes' => 'Yes', + 'Core:AttributeArchiveFlag' => 'Archive flag', + 'Core:AttributeArchiveFlag/Value:yes' => 'Yes', 'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode', - 'Core:AttributeArchiveFlag/Value:no' => 'No', - 'Core:AttributeArchiveFlag/Label' => 'Archived', - 'Core:AttributeArchiveFlag/Label+' => '', - 'Core:AttributeArchiveDate/Label' => 'Archive date', - 'Core:AttributeArchiveDate/Label+' => '', - - 'Core:AttributeObsolescenceFlag' => 'Obsolescence flag', - 'Core:AttributeObsolescenceFlag/Value:yes' => 'Yes', + 'Core:AttributeArchiveFlag/Value:no' => 'No', + 'Core:AttributeArchiveFlag/Label' => 'Archived', + 'Core:AttributeArchiveFlag/Label+' => '', + 'Core:AttributeArchiveDate/Label' => 'Archive date', + 'Core:AttributeArchiveDate/Label+' => '', + + 'Core:AttributeObsolescenceFlag' => 'Obsolescence flag', + 'Core:AttributeObsolescenceFlag/Value:yes' => 'Yes', 'Core:AttributeObsolescenceFlag/Value:yes+' => 'This object is excluded from the impact analysis, and hidden from search results', - 'Core:AttributeObsolescenceFlag/Value:no' => 'No', - 'Core:AttributeObsolescenceFlag/Label' => 'Obsolete', - 'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes', - 'Core:AttributeObsolescenceDate/Label' => 'Obsolescence date', - 'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete', + 'Core:AttributeObsolescenceFlag/Value:no' => 'No', + 'Core:AttributeObsolescenceFlag/Label' => 'Obsolete', + 'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes', + 'Core:AttributeObsolescenceDate/Label' => 'Obsolescence date', + 'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete', - 'Core:AttributeString' => 'String', + 'Core:AttributeString' => 'String', 'Core:AttributeString+' => 'Alphanumeric string', - 'Core:AttributeClass' => 'Class', + 'Core:AttributeClass' => 'Class', 'Core:AttributeClass+' => '', - 'Core:AttributeApplicationLanguage' => 'User language', + 'Core:AttributeApplicationLanguage' => 'User language', 'Core:AttributeApplicationLanguage+' => 'Language and country (EN US)', - 'Core:AttributeFinalClass' => 'Class (auto)', + 'Core:AttributeFinalClass' => 'Class (auto)', 'Core:AttributeFinalClass+' => 'Real class of the object (automatically created by the core)', - 'Core:AttributePassword' => 'Password', + 'Core:AttributePassword' => 'Password', 'Core:AttributePassword+' => 'Password of an external device', - 'Core:AttributeEncryptedString' => 'Encrypted string', - 'Core:AttributeEncryptedString+' => 'String encrypted with a local key', - 'Core:AttributeEncryptUnknownLibrary' => 'Encryption library specified (%1$s) unknown', + 'Core:AttributeEncryptedString' => 'Encrypted string', + 'Core:AttributeEncryptedString+' => 'String encrypted with a local key', + 'Core:AttributeEncryptUnknownLibrary' => 'Encryption library specified (%1$s) unknown', 'Core:AttributeEncryptFailedToDecrypt' => '** decryption error **', - 'Core:AttributeText' => 'Text', + 'Core:AttributeText' => 'Text', 'Core:AttributeText+' => 'Multiline character string', - 'Core:AttributeHTML' => 'HTML', + 'Core:AttributeHTML' => 'HTML', 'Core:AttributeHTML+' => 'HTML string', - 'Core:AttributeEmailAddress' => 'Email address', + 'Core:AttributeEmailAddress' => 'Email address', 'Core:AttributeEmailAddress+' => 'Email address', - 'Core:AttributeIPAddress' => 'IP address', + 'Core:AttributeIPAddress' => 'IP address', 'Core:AttributeIPAddress+' => 'IP address', - 'Core:AttributeOQL' => 'OQL', + 'Core:AttributeOQL' => 'OQL', 'Core:AttributeOQL+' => 'Object Query Language expression', - 'Core:AttributeEnum' => 'Enum', + 'Core:AttributeEnum' => 'Enum', 'Core:AttributeEnum+' => 'List of predefined alphanumeric strings', - 'Core:AttributeTemplateString' => 'Template string', + 'Core:AttributeTemplateString' => 'Template string', 'Core:AttributeTemplateString+' => 'String containing placeholders', - 'Core:AttributeTemplateText' => 'Template text', + 'Core:AttributeTemplateText' => 'Template text', 'Core:AttributeTemplateText+' => 'Text containing placeholders', - 'Core:AttributeTemplateHTML' => 'Template HTML', + 'Core:AttributeTemplateHTML' => 'Template HTML', 'Core:AttributeTemplateHTML+' => 'HTML containing placeholders', - 'Core:AttributeDateTime' => 'Date/time', - 'Core:AttributeDateTime+' => 'Date and time (year-month-day hh:mm:ss)', + 'Core:AttributeDateTime' => 'Date/time', + 'Core:AttributeDateTime+' => 'Date and time (year-month-day hh:mm:ss)', 'Core:AttributeDateTime?SmartSearch' => '

Date format:
@@ -159,8 +159,8 @@ If the time is omitted, it defaults to 00:00:00

', - 'Core:AttributeDate' => 'Date', - 'Core:AttributeDate+' => 'Date (year-month-day)', + 'Core:AttributeDate' => 'Date', + 'Core:AttributeDate+' => 'Date (year-month-day)', 'Core:AttributeDate?SmartSearch' => '

Date format:
@@ -174,60 +174,61 @@ [date,date]

', - 'Core:AttributeDeadline' => 'Deadline', + 'Core:AttributeDeadline' => 'Deadline', 'Core:AttributeDeadline+' => 'Date, displayed relatively to the current time', - 'Core:AttributeExternalKey' => 'External key', + 'Core:AttributeExternalKey' => 'External key', 'Core:AttributeExternalKey+' => 'External (or foreign) key', - 'Core:AttributeHierarchicalKey' => 'Hierarchical Key', + 'Core:AttributeHierarchicalKey' => 'Hierarchical Key', 'Core:AttributeHierarchicalKey+' => 'External (or foreign) key to the parent', - 'Core:AttributeExternalField' => 'External field', + 'Core:AttributeExternalField' => 'External field', 'Core:AttributeExternalField+' => 'Field mapped to an external key', - 'Core:AttributeURL' => 'URL', + 'Core:AttributeURL' => 'URL', 'Core:AttributeURL+' => 'Absolute or relative URL as a text string', - 'Core:AttributeBlob' => 'Blob', + 'Core:AttributeBlob' => 'Blob', 'Core:AttributeBlob+' => 'Any binary content (document)', - 'Core:AttributeOneWayPassword' => 'One way password', + 'Core:AttributeOneWayPassword' => 'One way password', 'Core:AttributeOneWayPassword+' => 'One way encrypted (hashed) password', - 'Core:AttributeTable' => 'Table', + 'Core:AttributeTable' => 'Table', 'Core:AttributeTable+' => 'Indexed array having two dimensions', - 'Core:AttributePropertySet' => 'Properties', + 'Core:AttributePropertySet' => 'Properties', 'Core:AttributePropertySet+' => 'List of untyped properties (name and value)', - 'Core:AttributeFriendlyName' => 'Friendly name', + 'Core:AttributeFriendlyName' => 'Friendly name', 'Core:AttributeFriendlyName+' => 'Attribute created automatically ; the friendly name is computed after several attributes', - 'Core:FriendlyName-Label' => 'Full name', + 'Core:FriendlyName-Label' => 'Full name', 'Core:FriendlyName-Description' => 'Full name', - 'Core:AttributeTag' => 'Tags', + 'Core:AttributeTag' => 'Tags', 'Core:AttributeTag+' => '', - 'Core:Context=REST/JSON' => 'REST', - 'Core:Context=Synchro' => 'Synchro', - 'Core:Context=Setup' => 'Setup', + 'Core:Context=REST/JSON' => 'REST', + 'Core:Context=Synchro' => 'Synchro', + 'Core:Context=Setup' => 'Setup', 'Core:Context=GUI:Console' => 'Console', - 'Core:Context=CRON' => 'cron', - 'Core:Context=GUI:Portal' => 'Portal', - - 'Core:GetCountingUsers:Error' => 'Error while getting %1$s quota', - 'Core:ConsoleUsers' => 'console users', - 'Core:DisabledUsers' => 'disabled users', - 'Core:PortalUsers' => 'portal users', - 'Core:BusinessPartnerUser' => 'business partner users', - 'Core:ReadOnlyUsers' => 'read-only users', - 'Core:ApplicationUsers' => 'application users', + 'Core:Context=CRON' => 'cron', + 'Core:Context=GUI:Portal' => 'Portal', + + 'Core:GetCountingUsers:Error' => 'Error while counting %1$s', + 'Core:CountingUsers:ConsoleUsers' => 'console users', + 'Core:CountingUsers:DisabledUsers' => 'disabled users', + 'Core:CountingUsers:PortalUsers' => 'portal users', + 'Core:CountingUsers:BusinessPartnerUser' => 'business partner users', + 'Core:CountingUsers:ReadOnlyUsers' => 'read - only users', + 'Core:CountingUsers:ApplicationUsers' => 'application users', + 'Core:CountingUsers:AllUsers' => 'all users', ]); ////////////////////////////////////////////////////////////////////// -// Classes in 'core/cmdb' +// Classes in 'core / cmdb' ////////////////////////////////////////////////////////////////////// // @@ -236,12 +237,12 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChange' => 'Change', - 'Class:CMDBChange+' => 'Changes tracking', - 'Class:CMDBChange/Attribute:date' => 'date', - 'Class:CMDBChange/Attribute:date+' => 'date and time at which the changes have been recorded', - 'Class:CMDBChange/Attribute:userinfo' => 'misc. info', - 'Class:CMDBChange/Attribute:userinfo+' => 'caller\'s defined information', + 'class: CMDBChange' => 'Change', + 'class: CMDBChange+' => 'Changes tracking', + 'class: CMDBChange/Attribute:date' => 'date', + 'class: CMDBChange/Attribute:date + ' => 'date and time at which the changes have been recorded', + 'class: CMDBChange/Attribute:userinfo' => 'misc.info', + 'class: CMDBChange/Attribute:userinfo + ' => 'caller\'s defined information', 'Class:CMDBChange/Attribute:origin/Value:interactive' => 'User interaction in the GUI', 'Class:CMDBChange/Attribute:origin/Value:csv-import.php' => 'CSV import script', 'Class:CMDBChange/Attribute:origin/Value:csv-interactive' => 'CSV import in the GUI', @@ -257,19 +258,19 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOp' => 'Change Operation', - 'Class:CMDBChangeOp+' => 'Change made by one person, at a single time, on a single object', - 'Class:CMDBChangeOp/Attribute:change' => 'change', - 'Class:CMDBChangeOp/Attribute:change+' => '', - 'Class:CMDBChangeOp/Attribute:date' => 'date', - 'Class:CMDBChangeOp/Attribute:date+' => 'date and time of the change', - 'Class:CMDBChangeOp/Attribute:userinfo' => 'user', - 'Class:CMDBChangeOp/Attribute:userinfo+' => 'who made this change', - 'Class:CMDBChangeOp/Attribute:objclass' => 'object class', - 'Class:CMDBChangeOp/Attribute:objclass+' => 'class name of the object on which the change was made', - 'Class:CMDBChangeOp/Attribute:objkey' => 'object id', - 'Class:CMDBChangeOp/Attribute:objkey+' => 'id of the object on which the change was made', - 'Class:CMDBChangeOp/Attribute:finalclass' => 'CMDBChangeOp sub-class', + 'Class:CMDBChangeOp' => 'Change Operation', + 'Class:CMDBChangeOp+' => 'Change made by one person, at a single time, on a single object', + 'Class:CMDBChangeOp/Attribute:change' => 'change', + 'Class:CMDBChangeOp/Attribute:change+' => '', + 'Class:CMDBChangeOp/Attribute:date' => 'date', + 'Class:CMDBChangeOp/Attribute:date+' => 'date and time of the change', + 'Class:CMDBChangeOp/Attribute:userinfo' => 'user', + 'Class:CMDBChangeOp/Attribute:userinfo+' => 'who made this change', + 'Class:CMDBChangeOp/Attribute:objclass' => 'object class', + 'Class:CMDBChangeOp/Attribute:objclass+' => 'class name of the object on which the change was made', + 'Class:CMDBChangeOp/Attribute:objkey' => 'object id', + 'Class:CMDBChangeOp/Attribute:objkey+' => 'id of the object on which the change was made', + 'Class:CMDBChangeOp/Attribute:finalclass' => 'CMDBChangeOp sub-class', 'Class:CMDBChangeOp/Attribute:finalclass+' => 'type of change which was performed', ]); @@ -278,7 +279,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpCreate' => 'object creation', + 'Class:CMDBChangeOpCreate' => 'object creation', 'Class:CMDBChangeOpCreate+' => 'Object creation tracking', ]); @@ -287,7 +288,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpDelete' => 'object deletion', + 'Class:CMDBChangeOpDelete' => 'object deletion', 'Class:CMDBChangeOpDelete+' => 'Object deletion tracking', ]); @@ -296,9 +297,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttribute' => 'object change', - 'Class:CMDBChangeOpSetAttribute+' => 'Object properties change tracking', - 'Class:CMDBChangeOpSetAttribute/Attribute:attcode' => 'Attribute', + 'Class:CMDBChangeOpSetAttribute' => 'object change', + 'Class:CMDBChangeOpSetAttribute+' => 'Object properties change tracking', + 'Class:CMDBChangeOpSetAttribute/Attribute:attcode' => 'Attribute', 'Class:CMDBChangeOpSetAttribute/Attribute:attcode+' => 'code of the modified property', ]); @@ -307,31 +308,31 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeScalar' => 'property change', - 'Class:CMDBChangeOpSetAttributeScalar+' => 'Object scalar properties change tracking', - 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue' => 'Previous value', + 'Class:CMDBChangeOpSetAttributeScalar' => 'property change', + 'Class:CMDBChangeOpSetAttributeScalar+' => 'Object scalar properties change tracking', + 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue' => 'Previous value', 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue+' => 'previous value of the attribute', - 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue' => 'New value', + 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue' => 'New value', 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue+' => 'new value of the attribute', ]); // Used by CMDBChangeOp... & derived classes Dict::Add('EN US', 'English', 'English', [ - 'Change:ObjectCreated' => 'Object created', - 'Change:ObjectDeleted' => 'Object deleted', - 'Change:ObjectModified' => 'Object modified', - 'Change:TwoAttributesChanged' => 'Edited %1$s and %2$s', - 'Change:ThreeAttributesChanged' => 'Edited %1$s, %2$s and 1 other', - 'Change:FourOrMoreAttributesChanged' => 'Edited %1$s, %2$s and %3$s others', + 'Change:ObjectCreated' => 'Object created', + 'Change:ObjectDeleted' => 'Object deleted', + 'Change:ObjectModified' => 'Object modified', + 'Change:TwoAttributesChanged' => 'Edited %1$s and %2$s', + 'Change:ThreeAttributesChanged' => 'Edited %1$s, %2$s and 1 other', + 'Change:FourOrMoreAttributesChanged' => 'Edited %1$s, %2$s and %3$s others', 'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s set to %2$s (previous value: %3$s)', - 'Change:AttName_SetTo' => '%1$s set to %2$s', - 'Change:Text_AppendedTo_AttName' => '%1$s appended to %2$s', - 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', - 'Change:AttName_Changed' => '%1$s modified', - 'Change:AttName_EntryAdded' => '%1$s modified, new entry added: %2$s', - 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s', - 'Change:LinkSet:Added' => 'added %1$s', - 'Change:LinkSet:Removed' => 'removed %1$s', - 'Change:LinkSet:Modified' => 'modified %1$s', + 'Change:AttName_SetTo' => '%1$s set to %2$s', + 'Change:Text_AppendedTo_AttName' => '%1$s appended to %2$s', + 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', + 'Change:AttName_Changed' => '%1$s modified', + 'Change:AttName_EntryAdded' => '%1$s modified, new entry added: %2$s', + 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s', + 'Change:LinkSet:Added' => 'added %1$s', + 'Change:LinkSet:Removed' => 'removed %1$s', + 'Change:LinkSet:Modified' => 'modified %1$s', ]); // @@ -339,9 +340,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeBlob' => 'data change', - 'Class:CMDBChangeOpSetAttributeBlob+' => 'data change tracking', - 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata' => 'Previous data', + 'Class:CMDBChangeOpSetAttributeBlob' => 'data change', + 'Class:CMDBChangeOpSetAttributeBlob+' => 'data change tracking', + 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata' => 'Previous data', 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata+' => 'previous contents of the attribute', ]); @@ -350,9 +351,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeText' => 'text change', - 'Class:CMDBChangeOpSetAttributeText+' => 'text change tracking', - 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata' => 'Previous data', + 'Class:CMDBChangeOpSetAttributeText' => 'text change', + 'Class:CMDBChangeOpSetAttributeText+' => 'text change tracking', + 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata' => 'Previous data', 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata+' => 'previous contents of the attribute', ]); @@ -361,15 +362,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Event' => 'Log Event', - 'Class:Event+' => 'An application internal event', - 'Class:Event/Attribute:message' => 'Message', - 'Class:Event/Attribute:message+' => 'short description of the event', - 'Class:Event/Attribute:date' => 'Date', - 'Class:Event/Attribute:date+' => 'date and time at which the changes have been recorded', - 'Class:Event/Attribute:userinfo' => 'User info', - 'Class:Event/Attribute:userinfo+' => 'identification of the user that was doing the action that triggered this event', - 'Class:Event/Attribute:finalclass' => 'Event sub-class', + 'Class:Event' => 'Log Event', + 'Class:Event+' => 'An application internal event', + 'Class:Event/Attribute:message' => 'Message', + 'Class:Event/Attribute:message+' => 'short description of the event', + 'Class:Event/Attribute:date' => 'Date', + 'Class:Event/Attribute:date+' => 'date and time at which the changes have been recorded', + 'Class:Event/Attribute:userinfo' => 'User info', + 'Class:Event/Attribute:userinfo+' => 'identification of the user that was doing the action that triggered this event', + 'Class:Event/Attribute:finalclass' => 'Event sub-class', 'Class:Event/Attribute:finalclass+' => 'Name of the final class: specifies the sort of event which occurred', ]); @@ -378,15 +379,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotification' => 'Notification sent', - 'Class:EventNotification+' => 'Trace of a notification that has been sent', - 'Class:EventNotification/Attribute:trigger_id' => 'Trigger', - 'Class:EventNotification/Attribute:trigger_id+' => '', - 'Class:EventNotification/Attribute:action_id' => 'Action', - 'Class:EventNotification/Attribute:action_id+' => '', - 'Class:EventNotification/Attribute:object_id' => 'Object id', - 'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)', - 'Class:EventNotification/Attribute:object_class' => 'Object class', + 'Class:EventNotification' => 'Notification sent', + 'Class:EventNotification+' => 'Trace of a notification that has been sent', + 'Class:EventNotification/Attribute:trigger_id' => 'Trigger', + 'Class:EventNotification/Attribute:trigger_id+' => '', + 'Class:EventNotification/Attribute:action_id' => 'Action', + 'Class:EventNotification/Attribute:action_id+' => '', + 'Class:EventNotification/Attribute:object_id' => 'Object id', + 'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)', + 'Class:EventNotification/Attribute:object_class' => 'Object class', 'Class:EventNotification/Attribute:object_class+' => 'Object class (Same as trigger)', ]); @@ -395,21 +396,21 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotificationEmail' => 'Email sent', - 'Class:EventNotificationEmail+' => 'Trace of an email that has been sent', - 'Class:EventNotificationEmail/Attribute:to' => 'TO', - 'Class:EventNotificationEmail/Attribute:to+' => '', - 'Class:EventNotificationEmail/Attribute:cc' => 'CC', - 'Class:EventNotificationEmail/Attribute:cc+' => '', - 'Class:EventNotificationEmail/Attribute:bcc' => 'BCC', - 'Class:EventNotificationEmail/Attribute:bcc+' => '', - 'Class:EventNotificationEmail/Attribute:from' => 'From', - 'Class:EventNotificationEmail/Attribute:from+' => 'Sender of the message', - 'Class:EventNotificationEmail/Attribute:subject' => 'Subject', - 'Class:EventNotificationEmail/Attribute:subject+' => '', - 'Class:EventNotificationEmail/Attribute:body' => 'Body', - 'Class:EventNotificationEmail/Attribute:body+' => '', - 'Class:EventNotificationEmail/Attribute:attachments' => 'Attachments', + 'Class:EventNotificationEmail' => 'Email sent', + 'Class:EventNotificationEmail+' => 'Trace of an email that has been sent', + 'Class:EventNotificationEmail/Attribute:to' => 'TO', + 'Class:EventNotificationEmail/Attribute:to+' => '', + 'Class:EventNotificationEmail/Attribute:cc' => 'CC', + 'Class:EventNotificationEmail/Attribute:cc+' => '', + 'Class:EventNotificationEmail/Attribute:bcc' => 'BCC', + 'Class:EventNotificationEmail/Attribute:bcc+' => '', + 'Class:EventNotificationEmail/Attribute:from' => 'From', + 'Class:EventNotificationEmail/Attribute:from+' => 'Sender of the message', + 'Class:EventNotificationEmail/Attribute:subject' => 'Subject', + 'Class:EventNotificationEmail/Attribute:subject+' => '', + 'Class:EventNotificationEmail/Attribute:body' => 'Body', + 'Class:EventNotificationEmail/Attribute:body+' => '', + 'Class:EventNotificationEmail/Attribute:attachments' => 'Attachments', 'Class:EventNotificationEmail/Attribute:attachments+' => '', ]); @@ -418,22 +419,22 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventIssue' => 'Issue event', - 'Class:EventIssue+' => 'Trace of an issue (warning, error, etc.)', - 'Class:EventIssue/Attribute:issue' => 'Issue', - 'Class:EventIssue/Attribute:issue+' => 'What happened', - 'Class:EventIssue/Attribute:impact' => 'Impact', - 'Class:EventIssue/Attribute:impact+' => 'What are the consequences', - 'Class:EventIssue/Attribute:page' => 'Page', - 'Class:EventIssue/Attribute:page+' => 'HTTP entry point', - 'Class:EventIssue/Attribute:arguments_post' => 'Posted arguments', + 'Class:EventIssue' => 'Issue event', + 'Class:EventIssue+' => 'Trace of an issue (warning, error, etc.)', + 'Class:EventIssue/Attribute:issue' => 'Issue', + 'Class:EventIssue/Attribute:issue+' => 'What happened', + 'Class:EventIssue/Attribute:impact' => 'Impact', + 'Class:EventIssue/Attribute:impact+' => 'What are the consequences', + 'Class:EventIssue/Attribute:page' => 'Page', + 'Class:EventIssue/Attribute:page+' => 'HTTP entry point', + 'Class:EventIssue/Attribute:arguments_post' => 'Posted arguments', 'Class:EventIssue/Attribute:arguments_post+' => 'HTTP POST arguments', - 'Class:EventIssue/Attribute:arguments_get' => 'URL arguments', - 'Class:EventIssue/Attribute:arguments_get+' => 'HTTP GET arguments', - 'Class:EventIssue/Attribute:callstack' => 'Callstack', - 'Class:EventIssue/Attribute:callstack+' => '', - 'Class:EventIssue/Attribute:data' => 'Data', - 'Class:EventIssue/Attribute:data+' => 'More information', + 'Class:EventIssue/Attribute:arguments_get' => 'URL arguments', + 'Class:EventIssue/Attribute:arguments_get+' => 'HTTP GET arguments', + 'Class:EventIssue/Attribute:callstack' => 'Callstack', + 'Class:EventIssue/Attribute:callstack+' => '', + 'Class:EventIssue/Attribute:data' => 'Data', + 'Class:EventIssue/Attribute:data+' => 'More information', ]); // @@ -441,37 +442,37 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventWebService' => 'Web service event', - 'Class:EventWebService+' => 'Trace of a web service call', - 'Class:EventWebService/Attribute:verb' => 'Verb', - 'Class:EventWebService/Attribute:verb+' => 'Name of the operation', - 'Class:EventWebService/Attribute:result' => 'Result', - 'Class:EventWebService/Attribute:result+' => 'Overall success/failure', - 'Class:EventWebService/Attribute:log_info' => 'Info log', - 'Class:EventWebService/Attribute:log_info+' => 'Result info log', - 'Class:EventWebService/Attribute:log_warning' => 'Warning log', + 'Class:EventWebService' => 'Web service event', + 'Class:EventWebService+' => 'Trace of a web service call', + 'Class:EventWebService/Attribute:verb' => 'Verb', + 'Class:EventWebService/Attribute:verb+' => 'Name of the operation', + 'Class:EventWebService/Attribute:result' => 'Result', + 'Class:EventWebService/Attribute:result+' => 'Overall success/failure', + 'Class:EventWebService/Attribute:log_info' => 'Info log', + 'Class:EventWebService/Attribute:log_info+' => 'Result info log', + 'Class:EventWebService/Attribute:log_warning' => 'Warning log', 'Class:EventWebService/Attribute:log_warning+' => 'Result warning log', - 'Class:EventWebService/Attribute:log_error' => 'Error log', - 'Class:EventWebService/Attribute:log_error+' => 'Result error log', - 'Class:EventWebService/Attribute:data' => 'Data', - 'Class:EventWebService/Attribute:data+' => 'Result data', + 'Class:EventWebService/Attribute:log_error' => 'Error log', + 'Class:EventWebService/Attribute:log_error+' => 'Result error log', + 'Class:EventWebService/Attribute:data' => 'Data', + 'Class:EventWebService/Attribute:data+' => 'Result data', ]); Dict::Add('EN US', 'English', 'English', [ - 'Class:EventRestService' => 'REST/JSON call', - 'Class:EventRestService+' => 'Trace of a REST/JSON service call', - 'Class:EventRestService/Attribute:operation' => 'Operation', - 'Class:EventRestService/Attribute:operation+' => 'Argument \'operation\'', - 'Class:EventRestService/Attribute:version' => 'Version', - 'Class:EventRestService/Attribute:version+' => 'Argument \'version\'', - 'Class:EventRestService/Attribute:json_input' => 'Input', - 'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'', - 'Class:EventRestService/Attribute:code' => 'Code', - 'Class:EventRestService/Attribute:code+' => 'Result code', - 'Class:EventRestService/Attribute:json_output' => 'Response', + 'Class:EventRestService' => 'REST/JSON call', + 'Class:EventRestService+' => 'Trace of a REST/JSON service call', + 'Class:EventRestService/Attribute:operation' => 'Operation', + 'Class:EventRestService/Attribute:operation+' => 'Argument \'operation\'', + 'Class:EventRestService/Attribute:version' => 'Version', + 'Class:EventRestService/Attribute:version+' => 'Argument \'version\'', + 'Class:EventRestService/Attribute:json_input' => 'Input', + 'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'', + 'Class:EventRestService/Attribute:code' => 'Code', + 'Class:EventRestService/Attribute:code+' => 'Result code', + 'Class:EventRestService/Attribute:json_output' => 'Response', 'Class:EventRestService/Attribute:json_output+' => 'HTTP response (json)', - 'Class:EventRestService/Attribute:provider' => 'Provider', - 'Class:EventRestService/Attribute:provider+' => 'PHP class implementing the expected operation', + 'Class:EventRestService/Attribute:provider' => 'Provider', + 'Class:EventRestService/Attribute:provider+' => 'PHP class implementing the expected operation', ]); // @@ -479,13 +480,13 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventLoginUsage' => 'Login Usage', - 'Class:EventLoginUsage+' => 'Connection to the application', - 'Class:EventLoginUsage/Attribute:user_id' => 'Login', - 'Class:EventLoginUsage/Attribute:user_id+' => '', - 'Class:EventLoginUsage/Attribute:contact_name' => 'User Name', - 'Class:EventLoginUsage/Attribute:contact_name+' => '', - 'Class:EventLoginUsage/Attribute:contact_email' => 'User Email', + 'Class:EventLoginUsage' => 'Login Usage', + 'Class:EventLoginUsage+' => 'Connection to the application', + 'Class:EventLoginUsage/Attribute:user_id' => 'Login', + 'Class:EventLoginUsage/Attribute:user_id+' => '', + 'Class:EventLoginUsage/Attribute:contact_name' => 'User Name', + 'Class:EventLoginUsage/Attribute:contact_name+' => '', + 'Class:EventLoginUsage/Attribute:contact_email' => 'User Email', 'Class:EventLoginUsage/Attribute:contact_email+' => 'Email Address of the User', ]); @@ -494,36 +495,36 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotificationNewsroom' => 'News sent', - 'Class:EventNotificationNewsroom+' => '', - 'Class:EventNotificationNewsroom/Attribute:title' => 'Title', - 'Class:EventNotificationNewsroom/Attribute:title+' => '', - 'Class:EventNotificationNewsroom/Attribute:icon' => 'Icon', - 'Class:EventNotificationNewsroom/Attribute:icon+' => '', - 'Class:EventNotificationNewsroom/Attribute:priority' => 'Priority', - 'Class:EventNotificationNewsroom/Attribute:priority+' => '', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:1' => 'Critical', + 'Class:EventNotificationNewsroom' => 'News sent', + 'Class:EventNotificationNewsroom+' => '', + 'Class:EventNotificationNewsroom/Attribute:title' => 'Title', + 'Class:EventNotificationNewsroom/Attribute:title+' => '', + 'Class:EventNotificationNewsroom/Attribute:icon' => 'Icon', + 'Class:EventNotificationNewsroom/Attribute:icon+' => '', + 'Class:EventNotificationNewsroom/Attribute:priority' => 'Priority', + 'Class:EventNotificationNewsroom/Attribute:priority+' => '', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:1' => 'Critical', 'Class:EventNotificationNewsroom/Attribute:priority/Value:1+' => 'Critical', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:2' => 'Urgent', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:2' => 'Urgent', 'Class:EventNotificationNewsroom/Attribute:priority/Value:2+' => 'Urgent', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:3' => 'Important', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:3' => 'Important', 'Class:EventNotificationNewsroom/Attribute:priority/Value:3+' => 'Important', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:4' => 'Standard', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:4' => 'Standard', 'Class:EventNotificationNewsroom/Attribute:priority/Value:4+' => 'Standard', - 'Class:EventNotificationNewsroom/Attribute:url' => 'URL', - 'Class:EventNotificationNewsroom/Attribute:url+' => '', - 'Class:EventNotificationNewsroom/Attribute:read' => 'Read', - 'Class:EventNotificationNewsroom/Attribute:read+' => '', - 'Class:EventNotificationNewsroom/Attribute:read/Value:no' => 'No', - 'Class:EventNotificationNewsroom/Attribute:read/Value:no+' => 'No', - 'Class:EventNotificationNewsroom/Attribute:read/Value:yes' => 'Yes', - 'Class:EventNotificationNewsroom/Attribute:read/Value:yes+' => 'Yes', - 'Class:EventNotificationNewsroom/Attribute:read_date' => 'Read date', - 'Class:EventNotificationNewsroom/Attribute:read_date+' => '', - 'Class:EventNotificationNewsroom/Attribute:contact_id' => 'Contact', - 'Class:EventNotificationNewsroom/Attribute:contact_id+' => '', - 'Core:EventNotificationNewsroom:ErrorNotificationNotSent' => 'Notification not sent', - 'Core:EventNotificationNewsroom:ErrorOnDBInsert' => 'An error occurred while saving the notification', + 'Class:EventNotificationNewsroom/Attribute:url' => 'URL', + 'Class:EventNotificationNewsroom/Attribute:url+' => '', + 'Class:EventNotificationNewsroom/Attribute:read' => 'Read', + 'Class:EventNotificationNewsroom/Attribute:read+' => '', + 'Class:EventNotificationNewsroom/Attribute:read/Value:no' => 'No', + 'Class:EventNotificationNewsroom/Attribute:read/Value:no+' => 'No', + 'Class:EventNotificationNewsroom/Attribute:read/Value:yes' => 'Yes', + 'Class:EventNotificationNewsroom/Attribute:read/Value:yes+' => 'Yes', + 'Class:EventNotificationNewsroom/Attribute:read_date' => 'Read date', + 'Class:EventNotificationNewsroom/Attribute:read_date+' => '', + 'Class:EventNotificationNewsroom/Attribute:contact_id' => 'Contact', + 'Class:EventNotificationNewsroom/Attribute:contact_id+' => '', + 'Core:EventNotificationNewsroom:ErrorNotificationNotSent' => 'Notification not sent', + 'Core:EventNotificationNewsroom:ErrorOnDBInsert' => 'An error occurred while saving the notification', ]); // @@ -531,35 +532,35 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Action' => 'Action', - 'Class:Action+' => 'User defined action', - 'Class:Action/ComplementaryName' => '%1$s: %2$s', - 'Class:Action/Attribute:name' => 'Name', - 'Class:Action/Attribute:name+' => 'Any value that is meaningful to distinguish this action from the others', - 'Class:Action/Attribute:description' => 'Description', - 'Class:Action/Attribute:description+' => 'A longer explanation about the purpose of this action. For information only.', - 'Class:Action/Attribute:status' => 'Status', - 'Class:Action/Attribute:status+' => 'This status drives the action behavior', - 'Class:Action/Attribute:status/Value:test' => 'Being tested', - 'Class:Action/Attribute:status/Value:test+' => '', - 'Class:Action/Attribute:status/Value:enabled' => 'In production', - 'Class:Action/Attribute:status/Value:enabled+' => '', - 'Class:Action/Attribute:status/Value:disabled' => 'Inactive', - 'Class:Action/Attribute:status/Value:disabled+' => '', - 'Class:Action/Attribute:trigger_list' => 'Related Triggers', - 'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action', - 'Class:Action/Attribute:asynchronous' => 'Asynchronous', - 'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in the background or not', + 'Class:Action' => 'Action', + 'Class:Action+' => 'User defined action', + 'Class:Action/ComplementaryName' => '%1$s: %2$s', + 'Class:Action/Attribute:name' => 'Name', + 'Class:Action/Attribute:name+' => 'Any value that is meaningful to distinguish this action from the others', + 'Class:Action/Attribute:description' => 'Description', + 'Class:Action/Attribute:description+' => 'A longer explanation about the purpose of this action. For information only.', + 'Class:Action/Attribute:status' => 'Status', + 'Class:Action/Attribute:status+' => 'This status drives the action behavior', + 'Class:Action/Attribute:status/Value:test' => 'Being tested', + 'Class:Action/Attribute:status/Value:test+' => '', + 'Class:Action/Attribute:status/Value:enabled' => 'In production', + 'Class:Action/Attribute:status/Value:enabled+' => '', + 'Class:Action/Attribute:status/Value:disabled' => 'Inactive', + 'Class:Action/Attribute:status/Value:disabled+' => '', + 'Class:Action/Attribute:trigger_list' => 'Related Triggers', + 'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action', + 'Class:Action/Attribute:asynchronous' => 'Asynchronous', + 'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in the background or not', 'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting', - 'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes', - 'Class:Action/Attribute:asynchronous/Value:no' => 'No', - 'Class:Action/Attribute:finalclass' => 'Action sub-class', - 'Class:Action/Attribute:finalclass+' => 'Name of the final class', - 'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.', - 'Action:last_executions_tab' => 'Last executions', - 'Action:last_executions_tab_panel_title' => 'Executions of this action (%1$s)', - 'Action:last_executions_tab_limit_days' => 'past %1$s days', - 'Action:last_executions_tab_limit_none' => 'no limit', + 'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes', + 'Class:Action/Attribute:asynchronous/Value:no' => 'No', + 'Class:Action/Attribute:finalclass' => 'Action sub-class', + 'Class:Action/Attribute:finalclass+' => 'Name of the final class', + 'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.', + 'Action:last_executions_tab' => 'Last executions', + 'Action:last_executions_tab_panel_title' => 'Executions of this action (%1$s)', + 'Action:last_executions_tab_limit_days' => 'past %1$s days', + 'Action:last_executions_tab_limit_none' => 'no limit', ]); // @@ -567,9 +568,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ActionNotification' => 'Notification Action', - 'Class:ActionNotification+' => 'Notification Action (abstract)', - 'Class:ActionNotification/Attribute:language' => 'Language', + 'Class:ActionNotification' => 'Notification Action', + 'Class:ActionNotification+' => 'Notification Action (abstract)', + 'Class:ActionNotification/Attribute:language' => 'Language', 'Class:ActionNotification/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)', ]); @@ -578,15 +579,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', - 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', - 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', + 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', + 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact who subscribed (or not) to the notification', - 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', - 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', - 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'The trigger that fired the notification', - 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'If the contact unsubscribed (no) or is subscribed (yes and default) to the notification', ]); @@ -670,40 +671,40 @@ // Dict::Add('EN US', 'English', 'English', [ - 'ActionNewsroom:trigger' => 'Trigger', - 'ActionNewsroom:content' => 'Message', - 'ActionNewsroom:settings' => 'Settings', - 'Class:ActionNewsroom' => 'Notification by Newsroom', - 'Class:ActionNewsroom+' => '', - 'Class:ActionNewsroom/Attribute:title' => 'Title', - 'Class:ActionNewsroom/Attribute:title+' => 'Title of the news. Can contain placeholders like $this->attribute_code$', - 'Class:ActionNewsroom/Attribute:message' => 'Message', - 'Class:ActionNewsroom/Attribute:message+' => 'Contents of the news, in Markdown format not HTML. Can contain placeholders like: + 'ActionNewsroom:trigger' => 'Trigger', + 'ActionNewsroom:content' => 'Message', + 'ActionNewsroom:settings' => 'Settings', + 'Class:ActionNewsroom' => 'Notification by Newsroom', + 'Class:ActionNewsroom+' => '', + 'Class:ActionNewsroom/Attribute:title' => 'Title', + 'Class:ActionNewsroom/Attribute:title+' => 'Title of the news. Can contain placeholders like $this->attribute_code$', + 'Class:ActionNewsroom/Attribute:message' => 'Message', + 'Class:ActionNewsroom/Attribute:message+' => 'Contents of the news, in Markdown format not HTML. Can contain placeholders like: - $this->attribute_code$ any attribute of the object triggering the notification, - $this->attribute_external_key->attribute$ recursive syntax for any remote attribute, - $current_contact->attribute$ attribute of the Person who triggered the notification', - 'Class:ActionNewsroom/Attribute:icon' => 'Icon', - 'Class:ActionNewsroom/Attribute:icon+' => 'Icon to appear next to the news in the newsroom. + 'Class:ActionNewsroom/Attribute:icon' => 'Icon', + 'Class:ActionNewsroom/Attribute:icon+' => 'Icon to appear next to the news in the newsroom. - If filled, the custom icon will be used - Else the icon of the triggering object if there is one (e.g. picture of a Person), - Else the icon of the triggering object class, - Otherwise, the application compact logo will be used', - 'Class:ActionNewsroom/Attribute:priority' => 'Priority', - 'Class:ActionNewsroom/Attribute:priority+' => 'News will be ordered by decreasing priority, when displayed in the Newsroom popup', - 'Class:ActionNewsroom/Attribute:priority/Value:1' => 'Critical', - 'Class:ActionNewsroom/Attribute:priority/Value:1+' => 'Critical', - 'Class:ActionNewsroom/Attribute:priority/Value:2' => 'Urgent', - 'Class:ActionNewsroom/Attribute:priority/Value:2+' => 'Urgent', - 'Class:ActionNewsroom/Attribute:priority/Value:3' => 'Important', - 'Class:ActionNewsroom/Attribute:priority/Value:3+' => 'Important', - 'Class:ActionNewsroom/Attribute:priority/Value:4' => 'Standard', - 'Class:ActionNewsroom/Attribute:priority/Value:4+' => 'Standard', - 'Class:ActionNewsroom/Attribute:test_recipient_id' => 'Test recipient', + 'Class:ActionNewsroom/Attribute:priority' => 'Priority', + 'Class:ActionNewsroom/Attribute:priority+' => 'News will be ordered by decreasing priority, when displayed in the Newsroom popup', + 'Class:ActionNewsroom/Attribute:priority/Value:1' => 'Critical', + 'Class:ActionNewsroom/Attribute:priority/Value:1+' => 'Critical', + 'Class:ActionNewsroom/Attribute:priority/Value:2' => 'Urgent', + 'Class:ActionNewsroom/Attribute:priority/Value:2+' => 'Urgent', + 'Class:ActionNewsroom/Attribute:priority/Value:3' => 'Important', + 'Class:ActionNewsroom/Attribute:priority/Value:3+' => 'Important', + 'Class:ActionNewsroom/Attribute:priority/Value:4' => 'Standard', + 'Class:ActionNewsroom/Attribute:priority/Value:4+' => 'Standard', + 'Class:ActionNewsroom/Attribute:test_recipient_id' => 'Test recipient', 'Class:ActionNewsroom/Attribute:test_recipient_id+' => 'Person used instead of Recipients when notification is being tested', - 'Class:ActionNewsroom/Attribute:recipients' => 'Recipients', - 'Class:ActionNewsroom/Attribute:recipients+' => 'An OQL query returning Contact objects', - 'Class:ActionNewsroom/Attribute:url' => 'URL', - 'Class:ActionNewsroom/Attribute:url+' => 'By default, it points to the object triggering the notification. But you can also specify a custom URL.', + 'Class:ActionNewsroom/Attribute:recipients' => 'Recipients', + 'Class:ActionNewsroom/Attribute:recipients+' => 'An OQL query returning Contact objects', + 'Class:ActionNewsroom/Attribute:url' => 'URL', + 'Class:ActionNewsroom/Attribute:url+' => 'By default, it points to the object triggering the notification. But you can also specify a custom URL.', ]); // @@ -711,24 +712,24 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Trigger' => 'Trigger', - 'Class:Trigger+' => 'Custom event handler', - 'Class:Trigger/ComplementaryName' => '%1$s, %2$s', - 'Class:Trigger/Attribute:description' => 'Description', - 'Class:Trigger/Attribute:description+' => 'Be precise as your users will base their potential unsubscription on this information', - 'Class:Trigger/Attribute:action_list' => 'Triggered actions', - 'Class:Trigger/Attribute:action_list+' => 'Actions performed when the trigger is activated', - 'Class:Trigger/Attribute:finalclass' => 'Trigger sub-class', - 'Class:Trigger/Attribute:finalclass+' => 'Name of the final class', - 'Class:Trigger/Attribute:context' => 'Context', - 'Class:Trigger/Attribute:context+' => 'Context to allow the trigger to start', - 'Class:Trigger/Attribute:complement' => 'Additional information', - 'Class:Trigger/Attribute:complement+' => 'Computed automatically in english for triggers derived from TriggerOnObject', - 'Class:Trigger/Attribute:subscription_policy' => 'Subscription policy', - 'Class:Trigger/Attribute:subscription_policy+' => 'Allows users to unsubscribe from the trigger', - 'Class:Trigger/Attribute:subscription_policy/Value:allow_no_channel' => 'Allow complete unsubscription', + 'Class:Trigger' => 'Trigger', + 'Class:Trigger+' => 'Custom event handler', + 'Class:Trigger/ComplementaryName' => '%1$s, %2$s', + 'Class:Trigger/Attribute:description' => 'Description', + 'Class:Trigger/Attribute:description+' => 'Be precise as your users will base their potential unsubscription on this information', + 'Class:Trigger/Attribute:action_list' => 'Triggered actions', + 'Class:Trigger/Attribute:action_list+' => 'Actions performed when the trigger is activated', + 'Class:Trigger/Attribute:finalclass' => 'Trigger sub-class', + 'Class:Trigger/Attribute:finalclass+' => 'Name of the final class', + 'Class:Trigger/Attribute:context' => 'Context', + 'Class:Trigger/Attribute:context+' => 'Context to allow the trigger to start', + 'Class:Trigger/Attribute:complement' => 'Additional information', + 'Class:Trigger/Attribute:complement+' => 'Computed automatically in english for triggers derived from TriggerOnObject', + 'Class:Trigger/Attribute:subscription_policy' => 'Subscription policy', + 'Class:Trigger/Attribute:subscription_policy+' => 'Allows users to unsubscribe from the trigger', + 'Class:Trigger/Attribute:subscription_policy/Value:allow_no_channel' => 'Allow complete unsubscription', 'Class:Trigger/Attribute:subscription_policy/Value:force_at_least_one_channel' => 'Force at least one channel (News or Email)', - 'Class:Trigger/Attribute:subscription_policy/Value:force_all_channels' => 'Deny unsubscription', + 'Class:Trigger/Attribute:subscription_policy/Value:force_all_channels' => 'Deny unsubscription', ]); // @@ -751,7 +752,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnPortalUpdate' => 'Trigger (when updated from the portal)', + 'Class:TriggerOnPortalUpdate' => 'Trigger (when updated from the portal)', 'Class:TriggerOnPortalUpdate+' => 'Trigger on an end-user\'s update from the portal', ]); @@ -760,9 +761,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateChange' => 'Trigger (on state change)', - 'Class:TriggerOnStateChange+' => 'Trigger on object state change', - 'Class:TriggerOnStateChange/Attribute:state' => 'State', + 'Class:TriggerOnStateChange' => 'Trigger (on state change)', + 'Class:TriggerOnStateChange+' => 'Trigger on object state change', + 'Class:TriggerOnStateChange/Attribute:state' => 'State', 'Class:TriggerOnStateChange/Attribute:state+' => '', ]); @@ -771,7 +772,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateEnter' => 'Trigger (on entering a state)', + 'Class:TriggerOnStateEnter' => 'Trigger (on entering a state)', 'Class:TriggerOnStateEnter+' => 'Trigger on object state change - entering', ]); @@ -780,7 +781,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateLeave' => 'Trigger (on leaving a state)', + 'Class:TriggerOnStateLeave' => 'Trigger (on leaving a state)', 'Class:TriggerOnStateLeave+' => 'Trigger on object state change - leaving', ]); @@ -789,7 +790,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectCreate' => 'Trigger (on object creation)', + 'Class:TriggerOnObjectCreate' => 'Trigger (on object creation)', 'Class:TriggerOnObjectCreate+' => 'Trigger on object creation of [a child class of] the given class', ]); @@ -798,7 +799,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectDelete' => 'Trigger (on object deletion)', + 'Class:TriggerOnObjectDelete' => 'Trigger (on object deletion)', 'Class:TriggerOnObjectDelete+' => 'Trigger on object deletion of [a child class of] the given class', ]); @@ -807,10 +808,10 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)', - 'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class', - 'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions', - 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields', + 'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)', + 'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class', + 'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions', + 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields', 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '', ]); @@ -819,9 +820,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectMention' => 'Trigger (on object mention)', - 'Class:TriggerOnObjectMention+' => 'Trigger on mention (@xxx) of an object of [a child class of] the given class in a log attribute', - 'Class:TriggerOnObjectMention/Attribute:mentioned_filter' => 'Mentioned filter', + 'Class:TriggerOnObjectMention' => 'Trigger (on object mention)', + 'Class:TriggerOnObjectMention+' => 'Trigger on mention (@xxx) of an object of [a child class of] the given class in a log attribute', + 'Class:TriggerOnObjectMention/Attribute:mentioned_filter' => 'Mentioned filter', 'Class:TriggerOnObjectMention/Attribute:mentioned_filter+' => 'Limit the list of mentioned objects which will activate the trigger. If empty, any mentioned object (of any class) will activate it.', ]); @@ -830,9 +831,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)', - 'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class', - 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields', + 'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)', + 'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class', + 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields', 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes+' => '', ]); @@ -841,11 +842,11 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnThresholdReached' => 'Trigger (on threshold)', - 'Class:TriggerOnThresholdReached+' => 'Trigger on Stop-Watch threshold reached', - 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => 'Stop watch', + 'Class:TriggerOnThresholdReached' => 'Trigger (on threshold)', + 'Class:TriggerOnThresholdReached+' => 'Trigger on Stop-Watch threshold reached', + 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => 'Stop watch', 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code+' => '', - 'Class:TriggerOnThresholdReached/Attribute:threshold_index' => 'Threshold', + 'Class:TriggerOnThresholdReached/Attribute:threshold_index' => 'Threshold', 'Class:TriggerOnThresholdReached/Attribute:threshold_index+' => '', ]); @@ -854,18 +855,18 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:lnkTriggerAction' => 'Action/Trigger', - 'Class:lnkTriggerAction+' => 'Link between a trigger and an action', - 'Class:lnkTriggerAction/Attribute:action_id' => 'Action', - 'Class:lnkTriggerAction/Attribute:action_id+' => 'The action to be executed', - 'Class:lnkTriggerAction/Attribute:action_name' => 'Action', - 'Class:lnkTriggerAction/Attribute:action_name+' => '', - 'Class:lnkTriggerAction/Attribute:trigger_id' => 'Trigger', - 'Class:lnkTriggerAction/Attribute:trigger_id+' => '', - 'Class:lnkTriggerAction/Attribute:trigger_name' => 'Trigger', + 'Class:lnkTriggerAction' => 'Action/Trigger', + 'Class:lnkTriggerAction+' => 'Link between a trigger and an action', + 'Class:lnkTriggerAction/Attribute:action_id' => 'Action', + 'Class:lnkTriggerAction/Attribute:action_id+' => 'The action to be executed', + 'Class:lnkTriggerAction/Attribute:action_name' => 'Action', + 'Class:lnkTriggerAction/Attribute:action_name+' => '', + 'Class:lnkTriggerAction/Attribute:trigger_id' => 'Trigger', + 'Class:lnkTriggerAction/Attribute:trigger_id+' => '', + 'Class:lnkTriggerAction/Attribute:trigger_name' => 'Trigger', 'Class:lnkTriggerAction/Attribute:trigger_name+' => '', - 'Class:lnkTriggerAction/Attribute:order' => 'Order', - 'Class:lnkTriggerAction/Attribute:order+' => 'Actions execution order', + 'Class:lnkTriggerAction/Attribute:order' => 'Order', + 'Class:lnkTriggerAction/Attribute:order+' => 'Actions execution order', ]); // @@ -1060,162 +1061,162 @@ 'Class:SynchroLog/Attribute:stats_nb_obj_created' => 'Nb objects created', 'Class:SynchroLog/Attribute:stats_nb_obj_created_errors' => 'Nb or errors while creating', 'Class:SynchroLog/Attribute:stats_nb_obj_updated' => 'Nb objects updated', - 'Class:SynchroLog/Attribute:stats_nb_obj_updated_errors' => 'Nb errors while updating', - 'Class:SynchroLog/Attribute:stats_nb_replica_reconciled_errors' => 'Nb of errors during reconciliation', - 'Class:SynchroLog/Attribute:stats_nb_replica_disappeared_no_action' => 'Nb replica disappeared', - 'Class:SynchroLog/Attribute:stats_nb_obj_new_updated' => 'Nb objects updated', - 'Class:SynchroLog/Attribute:stats_nb_obj_new_unchanged' => 'Nb objects unchanged', - 'Class:SynchroLog/Attribute:last_error' => 'Last error', - 'Class:SynchroLog/Attribute:traces' => 'Traces', - 'Class:SynchroReplica' => 'Synchro Replica', - 'Class:SynchroReplica/Attribute:sync_source_id' => 'Synchro Data Source', - 'Class:SynchroReplica/Attribute:dest_id' => 'Destination object (ID)', - 'Class:SynchroReplica/Attribute:dest_class' => 'Destination type', - 'Class:SynchroReplica/Attribute:status_last_seen' => 'Last seen', - 'Class:SynchroReplica/Attribute:status' => 'Status', - 'Class:SynchroReplica/Attribute:status/Value:modified' => 'Modified', - 'Class:SynchroReplica/Attribute:status/Value:new' => 'New', - 'Class:SynchroReplica/Attribute:status/Value:obsolete' => 'Obsolete', - 'Class:SynchroReplica/Attribute:status/Value:orphan' => 'Orphan', - 'Class:SynchroReplica/Attribute:status/Value:synchronized' => 'Synchronized', - 'Class:SynchroReplica/Attribute:status_dest_creator' => 'Object Created ?', - 'Class:SynchroReplica/Attribute:status_last_error' => 'Last Error', - 'Class:SynchroReplica/Attribute:status_last_warning' => 'Warnings', - 'Class:SynchroReplica/Attribute:info_creation_date' => 'Creation Date', - 'Class:SynchroReplica/Attribute:info_last_modified' => 'Last Modified Date', - 'Class:appUserPreferences' => 'User Preferences', - 'Class:appUserPreferences/Attribute:userid' => 'User', - 'Class:appUserPreferences/Attribute:preferences' => 'Prefs', - 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)', - 'Core:ExecProcess:Code255' => 'PHP Error (parsing, or runtime)', + 'Class:SynchroLog/Attribute:stats_nb_obj_updated_errors' => 'Nb errors while updating', + 'Class:SynchroLog/Attribute:stats_nb_replica_reconciled_errors' => 'Nb of errors during reconciliation', + 'Class:SynchroLog/Attribute:stats_nb_replica_disappeared_no_action' => 'Nb replica disappeared', + 'Class:SynchroLog/Attribute:stats_nb_obj_new_updated' => 'Nb objects updated', + 'Class:SynchroLog/Attribute:stats_nb_obj_new_unchanged' => 'Nb objects unchanged', + 'Class:SynchroLog/Attribute:last_error' => 'Last error', + 'Class:SynchroLog/Attribute:traces' => 'Traces', + 'Class:SynchroReplica' => 'Synchro Replica', + 'Class:SynchroReplica/Attribute:sync_source_id' => 'Synchro Data Source', + 'Class:SynchroReplica/Attribute:dest_id' => 'Destination object (ID)', + 'Class:SynchroReplica/Attribute:dest_class' => 'Destination type', + 'Class:SynchroReplica/Attribute:status_last_seen' => 'Last seen', + 'Class:SynchroReplica/Attribute:status' => 'Status', + 'Class:SynchroReplica/Attribute:status/Value:modified' => 'Modified', + 'Class:SynchroReplica/Attribute:status/Value:new' => 'New', + 'Class:SynchroReplica/Attribute:status/Value:obsolete' => 'Obsolete', + 'Class:SynchroReplica/Attribute:status/Value:orphan' => 'Orphan', + 'Class:SynchroReplica/Attribute:status/Value:synchronized' => 'Synchronized', + 'Class:SynchroReplica/Attribute:status_dest_creator' => 'Object Created ?', + 'Class:SynchroReplica/Attribute:status_last_error' => 'Last Error', + 'Class:SynchroReplica/Attribute:status_last_warning' => 'Warnings', + 'Class:SynchroReplica/Attribute:info_creation_date' => 'Creation Date', + 'Class:SynchroReplica/Attribute:info_last_modified' => 'Last Modified Date', + 'Class:appUserPreferences' => 'User Preferences', + 'Class:appUserPreferences/Attribute:userid' => 'User', + 'Class:appUserPreferences/Attribute:preferences' => 'Prefs', + 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)', + 'Core:ExecProcess:Code255' => 'PHP Error (parsing, or runtime)', // Attribute Duration - 'Core:Duration_Seconds' => '%1$ds', - 'Core:Duration_Minutes_Seconds' => '%1$dmin %2$ds', - 'Core:Duration_Hours_Minutes_Seconds' => '%1$dh %2$dmin %3$ds', - 'Core:Duration_Days_Hours_Minutes_Seconds' => '%1$sd %2$dh %3$dmin %4$ds', + 'Core:Duration_Seconds' => '%1$ds', + 'Core:Duration_Minutes_Seconds' => '%1$dmin %2$ds', + 'Core:Duration_Hours_Minutes_Seconds' => '%1$dh %2$dmin %3$ds', + 'Core:Duration_Days_Hours_Minutes_Seconds' => '%1$sd %2$dh %3$dmin %4$ds', // Explain working time computing - 'Core:ExplainWTC:ElapsedTime' => 'Time elapsed (stored as "%1$s")', - 'Core:ExplainWTC:StopWatch-TimeSpent' => 'Time spent for "%1$s"', - 'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%', + 'Core:ExplainWTC:ElapsedTime' => 'Time elapsed (stored as "%1$s")', + 'Core:ExplainWTC:StopWatch-TimeSpent' => 'Time spent for "%1$s"', + 'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%', // Bulk export - 'Core:BulkExport:MissingParameter_Param' => 'Missing parameter "%1$s"', - 'Core:BulkExport:InvalidParameter_Query' => 'Invalid value for the parameter "query". There is no Query Phrasebook corresponding to the id: "%1$s".', - 'Core:BulkExport:ExportFormatPrompt' => 'Export format:', - 'Core:BulkExportOf_Class' => '%1$s Export', - 'Core:BulkExport:ClickHereToDownload_FileName' => 'Click here to download %1$s', - 'Core:BulkExport:ExportResult' => 'Result of the export:', - 'Core:BulkExport:RetrievingData' => 'Retrieving data...', - 'Core:BulkExport:HTMLFormat' => 'Web Page (*.html)', - 'Core:BulkExport:CSVFormat' => 'Comma Separated Values (*.csv)', - 'Core:BulkExport:XLSXFormat' => 'Excel 2007 or newer (*.xlsx)', - 'Core:BulkExport:PDFFormat' => 'PDF Document (*.pdf)', - 'Core:BulkExport:DragAndDropHelp' => 'Drag and drop the columns\' headers to arrange the columns. Preview of %1$s lines. Total number of lines to export: %2$s.', - 'Core:BulkExport:EmptyPreview' => 'Select the columns to be exported from the list above', - 'Core:BulkExport:ColumnsOrder' => 'Columns order', - 'Core:BulkExport:AvailableColumnsFrom_Class' => 'Available columns from %1$s', - 'Core:BulkExport:NoFieldSelected' => 'Select at least one column to be exported', - 'Core:BulkExport:CheckAll' => 'Check All', - 'Core:BulkExport:UncheckAll' => 'Uncheck All', - 'Core:BulkExport:ExportCancelledByUser' => 'Export cancelled by the user', - 'Core:BulkExport:CSVOptions' => 'CSV Options', - 'Core:BulkExport:CSVLocalization' => 'Localization', - 'Core:BulkExport:PDFOptions' => 'PDF Options', - 'Core:BulkExport:PDFPageFormat' => 'Page Format', - 'Core:BulkExport:PDFPageSize' => 'Page Size:', - 'Core:BulkExport:PageSize-A4' => 'A4', - 'Core:BulkExport:PageSize-A3' => 'A3', - 'Core:BulkExport:PageSize-Letter' => 'Letter', - 'Core:BulkExport:PDFPageOrientation' => 'Page Orientation:', - 'Core:BulkExport:PageOrientation-L' => 'Landscape', - 'Core:BulkExport:PageOrientation-P' => 'Portrait', - 'Core:BulkExport:XMLFormat' => 'XML file (*.xml)', - 'Core:BulkExport:XMLOptions' => 'XML Options', - 'Core:BulkExport:SpreadsheetFormat' => 'Spreadsheet HTML format (*.html)', - 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options', - 'Core:BulkExport:OptionNoLocalize' => 'Export Code instead of Label', - 'Core:BulkExport:OptionLinkSets' => 'Include linked objects', - 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting', - 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export', - 'Core:BulkExportLabelOQLExpression' => 'OQL Query:', - 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:', - 'Core:BulkExportMessageEmptyOQL' => 'Please enter a valid OQL query.', - 'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Please select a valid phrasebook entry.', - 'Core:BulkExportQueryPlaceholder' => 'Type an OQL query here...', - 'Core:BulkExportCanRunNonInteractive' => 'Click here to run the export in non-interactive mode.', - 'Core:BulkExportLegacyExport' => 'Click here to access the legacy export.', - 'Core:BulkExport:XLSXOptions' => 'Excel Options', - 'Core:BulkExport:TextFormat' => 'Text fields containing some HTML markup', - 'Core:BulkExport:DateTimeFormat' => 'Date and Time format', - 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Default format (%1$s), e.g. %2$s', - 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Custom format: %1$s', - 'Core:BulkExport:PDF:PageNumber' => 'Page %1$s', - 'Core:DateTime:Placeholder_d' => 'DD', // Day of the month: 2 digits (with leading zero) - 'Core:DateTime:Placeholder_j' => 'D', // Day of the month: 1 or 2 digits (without leading zero) - 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 - 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 - 'Core:DateTime:Placeholder_Y' => 'YYYY', // Year on 4 digits - 'Core:DateTime:Placeholder_y' => 'YY', // Year on 2 digits - 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 - 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 - 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 - 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 - 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) - 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) - 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 - 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 - 'Core:Validator:Default' => 'Wrong format', - 'Core:Validator:Mandatory' => 'Please, fill this field', - 'Core:Validator:MustBeInteger' => 'Must be an integer', - 'Core:Validator:MustSelectOne' => 'Please, select one', + 'Core:BulkExport:MissingParameter_Param' => 'Missing parameter "%1$s"', + 'Core:BulkExport:InvalidParameter_Query' => 'Invalid value for the parameter "query". There is no Query Phrasebook corresponding to the id: "%1$s".', + 'Core:BulkExport:ExportFormatPrompt' => 'Export format:', + 'Core:BulkExportOf_Class' => '%1$s Export', + 'Core:BulkExport:ClickHereToDownload_FileName' => 'Click here to download %1$s', + 'Core:BulkExport:ExportResult' => 'Result of the export:', + 'Core:BulkExport:RetrievingData' => 'Retrieving data...', + 'Core:BulkExport:HTMLFormat' => 'Web Page (*.html)', + 'Core:BulkExport:CSVFormat' => 'Comma Separated Values (*.csv)', + 'Core:BulkExport:XLSXFormat' => 'Excel 2007 or newer (*.xlsx)', + 'Core:BulkExport:PDFFormat' => 'PDF Document (*.pdf)', + 'Core:BulkExport:DragAndDropHelp' => 'Drag and drop the columns\' headers to arrange the columns. Preview of %1$s lines. Total number of lines to export: %2$s.', + 'Core:BulkExport:EmptyPreview' => 'Select the columns to be exported from the list above', + 'Core:BulkExport:ColumnsOrder' => 'Columns order', + 'Core:BulkExport:AvailableColumnsFrom_Class' => 'Available columns from %1$s', + 'Core:BulkExport:NoFieldSelected' => 'Select at least one column to be exported', + 'Core:BulkExport:CheckAll' => 'Check All', + 'Core:BulkExport:UncheckAll' => 'Uncheck All', + 'Core:BulkExport:ExportCancelledByUser' => 'Export cancelled by the user', + 'Core:BulkExport:CSVOptions' => 'CSV Options', + 'Core:BulkExport:CSVLocalization' => 'Localization', + 'Core:BulkExport:PDFOptions' => 'PDF Options', + 'Core:BulkExport:PDFPageFormat' => 'Page Format', + 'Core:BulkExport:PDFPageSize' => 'Page Size:', + 'Core:BulkExport:PageSize-A4' => 'A4', + 'Core:BulkExport:PageSize-A3' => 'A3', + 'Core:BulkExport:PageSize-Letter' => 'Letter', + 'Core:BulkExport:PDFPageOrientation' => 'Page Orientation:', + 'Core:BulkExport:PageOrientation-L' => 'Landscape', + 'Core:BulkExport:PageOrientation-P' => 'Portrait', + 'Core:BulkExport:XMLFormat' => 'XML file (*.xml)', + 'Core:BulkExport:XMLOptions' => 'XML Options', + 'Core:BulkExport:SpreadsheetFormat' => 'Spreadsheet HTML format (*.html)', + 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options', + 'Core:BulkExport:OptionNoLocalize' => 'Export Code instead of Label', + 'Core:BulkExport:OptionLinkSets' => 'Include linked objects', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting', + 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export', + 'Core:BulkExportLabelOQLExpression' => 'OQL Query:', + 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:', + 'Core:BulkExportMessageEmptyOQL' => 'Please enter a valid OQL query.', + 'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Please select a valid phrasebook entry.', + 'Core:BulkExportQueryPlaceholder' => 'Type an OQL query here...', + 'Core:BulkExportCanRunNonInteractive' => 'Click here to run the export in non-interactive mode.', + 'Core:BulkExportLegacyExport' => 'Click here to access the legacy export.', + 'Core:BulkExport:XLSXOptions' => 'Excel Options', + 'Core:BulkExport:TextFormat' => 'Text fields containing some HTML markup', + 'Core:BulkExport:DateTimeFormat' => 'Date and Time format', + 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Default format (%1$s), e.g. %2$s', + 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Custom format: %1$s', + 'Core:BulkExport:PDF:PageNumber' => 'Page %1$s', + 'Core:DateTime:Placeholder_d' => 'DD', // Day of the month: 2 digits (with leading zero) + 'Core:DateTime:Placeholder_j' => 'D', // Day of the month: 1 or 2 digits (without leading zero) + 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 + 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 + 'Core:DateTime:Placeholder_Y' => 'YYYY', // Year on 4 digits + 'Core:DateTime:Placeholder_y' => 'YY', // Year on 2 digits + 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 + 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 + 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 + 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 + 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) + 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) + 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 + 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 + 'Core:Validator:Default' => 'Wrong format', + 'Core:Validator:Mandatory' => 'Please, fill this field', + 'Core:Validator:MustBeInteger' => 'Must be an integer', + 'Core:Validator:MustSelectOne' => 'Please, select one', ]); // // Class: TagSetFieldData // Dict::Add('EN US', 'English', 'English', [ - 'Class:TagSetFieldData' => '%2$s for class %1$s', + 'Class:TagSetFieldData' => '%2$s for class %1$s', 'Class:TagSetFieldData+' => '', - 'Class:TagSetFieldData/Attribute:code' => 'Code', - 'Class:TagSetFieldData/Attribute:code+' => 'Internal code. Must contain at least 3 alphanumeric characters', - 'Class:TagSetFieldData/Attribute:label' => 'Label', - 'Class:TagSetFieldData/Attribute:label+' => 'Displayed label', - 'Class:TagSetFieldData/Attribute:description' => 'Description', + 'Class:TagSetFieldData/Attribute:code' => 'Code', + 'Class:TagSetFieldData/Attribute:code+' => 'Internal code. Must contain at least 3 alphanumeric characters', + 'Class:TagSetFieldData/Attribute:label' => 'Label', + 'Class:TagSetFieldData/Attribute:label+' => 'Displayed label', + 'Class:TagSetFieldData/Attribute:description' => 'Description', 'Class:TagSetFieldData/Attribute:description+' => '', - 'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class', - 'Class:TagSetFieldData/Attribute:obj_class' => 'Object class', - 'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code', + 'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class', + 'Class:TagSetFieldData/Attribute:obj_class' => 'Object class', + 'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code', - 'Core:TagSetFieldData:ErrorDeleteUsedTag' => 'Used tags cannot be deleted', + 'Core:TagSetFieldData:ErrorDeleteUsedTag' => 'Used tags cannot be deleted', 'Core:TagSetFieldData:ErrorDuplicateTagCodeOrLabel' => 'Tags codes or labels must be unique', - 'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.', - 'Core:TagSetFieldData:ErrorTagCodeReservedWord' => 'The chosen tag code is a reserved word', - 'Core:TagSetFieldData:ErrorTagLabelSyntax' => 'Tags label must not contain \'%1$s\' nor be empty', - 'Core:TagSetFieldData:ErrorCodeUpdateNotAllowed' => 'Tags Code cannot be changed when used', - 'Core:TagSetFieldData:ErrorClassUpdateNotAllowed' => 'Tags "Object Class" cannot be changed', + 'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.', + 'Core:TagSetFieldData:ErrorTagCodeReservedWord' => 'The chosen tag code is a reserved word', + 'Core:TagSetFieldData:ErrorTagLabelSyntax' => 'Tags label must not contain \'%1$s\' nor be empty', + 'Core:TagSetFieldData:ErrorCodeUpdateNotAllowed' => 'Tags Code cannot be changed when used', + 'Core:TagSetFieldData:ErrorClassUpdateNotAllowed' => 'Tags "Object Class" cannot be changed', 'Core:TagSetFieldData:ErrorAttCodeUpdateNotAllowed' => 'Tags "Attribute Code" cannot be changed', - 'Core:TagSetFieldData:WhereIsThisTagTab' => 'Tag usage (%1$d)', - 'Core:TagSetFieldData:NoEntryFound' => 'No entry found for this tag', + 'Core:TagSetFieldData:WhereIsThisTagTab' => 'Tag usage (%1$d)', + 'Core:TagSetFieldData:NoEntryFound' => 'No entry found for this tag', ]); // // Class: DBProperty // Dict::Add('EN US', 'English', 'English', [ - 'Class:DBProperty' => 'DB property', - 'Class:DBProperty+' => '', - 'Class:DBProperty/Attribute:name' => 'Name', - 'Class:DBProperty/Attribute:name+' => '', - 'Class:DBProperty/Attribute:description' => 'Description', - 'Class:DBProperty/Attribute:description+' => '', - 'Class:DBProperty/Attribute:value' => 'Value', - 'Class:DBProperty/Attribute:value+' => '', - 'Class:DBProperty/Attribute:change_date' => 'Change date', - 'Class:DBProperty/Attribute:change_date+' => '', - 'Class:DBProperty/Attribute:change_comment' => 'Change comment', + 'Class:DBProperty' => 'DB property', + 'Class:DBProperty+' => '', + 'Class:DBProperty/Attribute:name' => 'Name', + 'Class:DBProperty/Attribute:name+' => '', + 'Class:DBProperty/Attribute:description' => 'Description', + 'Class:DBProperty/Attribute:description+' => '', + 'Class:DBProperty/Attribute:value' => 'Value', + 'Class:DBProperty/Attribute:value+' => '', + 'Class:DBProperty/Attribute:change_date' => 'Change date', + 'Class:DBProperty/Attribute:change_date+' => '', + 'Class:DBProperty/Attribute:change_comment' => 'Change comment', 'Class:DBProperty/Attribute:change_comment+' => '', ]); @@ -1223,59 +1224,59 @@ // Class: BackgroundTask // Dict::Add('EN US', 'English', 'English', [ - 'Class:BackgroundTask' => 'Background task', - 'Class:BackgroundTask+' => '', - 'Class:BackgroundTask/Attribute:class_name' => 'Class name', - 'Class:BackgroundTask/Attribute:class_name+' => '', - 'Class:BackgroundTask/Attribute:first_run_date' => 'First run date', - 'Class:BackgroundTask/Attribute:first_run_date+' => '', - 'Class:BackgroundTask/Attribute:latest_run_date' => 'Latest run date', - 'Class:BackgroundTask/Attribute:latest_run_date+' => '', - 'Class:BackgroundTask/Attribute:next_run_date' => 'Next run date', - 'Class:BackgroundTask/Attribute:next_run_date+' => '', - 'Class:BackgroundTask/Attribute:total_exec_count' => 'Total exec. count', - 'Class:BackgroundTask/Attribute:total_exec_count+' => '', - 'Class:BackgroundTask/Attribute:latest_run_duration' => 'Latest run duration', - 'Class:BackgroundTask/Attribute:latest_run_duration+' => '', - 'Class:BackgroundTask/Attribute:min_run_duration' => 'Min. run duration', - 'Class:BackgroundTask/Attribute:min_run_duration+' => '', - 'Class:BackgroundTask/Attribute:max_run_duration' => 'Max. run duration', - 'Class:BackgroundTask/Attribute:max_run_duration+' => '', - 'Class:BackgroundTask/Attribute:average_run_duration' => 'Average run duration', + 'Class:BackgroundTask' => 'Background task', + 'Class:BackgroundTask+' => '', + 'Class:BackgroundTask/Attribute:class_name' => 'Class name', + 'Class:BackgroundTask/Attribute:class_name+' => '', + 'Class:BackgroundTask/Attribute:first_run_date' => 'First run date', + 'Class:BackgroundTask/Attribute:first_run_date+' => '', + 'Class:BackgroundTask/Attribute:latest_run_date' => 'Latest run date', + 'Class:BackgroundTask/Attribute:latest_run_date+' => '', + 'Class:BackgroundTask/Attribute:next_run_date' => 'Next run date', + 'Class:BackgroundTask/Attribute:next_run_date+' => '', + 'Class:BackgroundTask/Attribute:total_exec_count' => 'Total exec. count', + 'Class:BackgroundTask/Attribute:total_exec_count+' => '', + 'Class:BackgroundTask/Attribute:latest_run_duration' => 'Latest run duration', + 'Class:BackgroundTask/Attribute:latest_run_duration+' => '', + 'Class:BackgroundTask/Attribute:min_run_duration' => 'Min. run duration', + 'Class:BackgroundTask/Attribute:min_run_duration+' => '', + 'Class:BackgroundTask/Attribute:max_run_duration' => 'Max. run duration', + 'Class:BackgroundTask/Attribute:max_run_duration+' => '', + 'Class:BackgroundTask/Attribute:average_run_duration' => 'Average run duration', 'Class:BackgroundTask/Attribute:average_run_duration+' => '', - 'Class:BackgroundTask/Attribute:running' => 'Running', - 'Class:BackgroundTask/Attribute:running+' => '', - 'Class:BackgroundTask/Attribute:status' => 'Status', - 'Class:BackgroundTask/Attribute:status+' => '', + 'Class:BackgroundTask/Attribute:running' => 'Running', + 'Class:BackgroundTask/Attribute:running+' => '', + 'Class:BackgroundTask/Attribute:status' => 'Status', + 'Class:BackgroundTask/Attribute:status+' => '', ]); // // Class: AsyncTask // Dict::Add('EN US', 'English', 'English', [ - 'Class:AsyncTask' => 'Async. task', - 'Class:AsyncTask+' => '', - 'Class:AsyncTask/Attribute:created' => 'Created', - 'Class:AsyncTask/Attribute:created+' => '', - 'Class:AsyncTask/Attribute:started' => 'Started', - 'Class:AsyncTask/Attribute:started+' => '', - 'Class:AsyncTask/Attribute:planned' => 'Planned', - 'Class:AsyncTask/Attribute:planned+' => '', - 'Class:AsyncTask/Attribute:event_id' => 'Event', - 'Class:AsyncTask/Attribute:event_id+' => '', - 'Class:AsyncTask/Attribute:finalclass' => 'Final class', - 'Class:AsyncTask/Attribute:finalclass+' => '', - 'Class:AsyncTask/Attribute:status' => 'Status', - 'Class:AsyncTask/Attribute:status+' => '', - 'Class:AsyncTask/Attribute:remaining_retries' => 'Remaining retries', - 'Class:AsyncTask/Attribute:remaining_retries+' => '', - 'Class:AsyncTask/Attribute:last_error_code' => 'Last error code', - 'Class:AsyncTask/Attribute:last_error_code+' => '', - 'Class:AsyncTask/Attribute:last_error' => 'Last error', - 'Class:AsyncTask/Attribute:last_error+' => '', - 'Class:AsyncTask/Attribute:last_attempt' => 'Last attempt', - 'Class:AsyncTask/Attribute:last_attempt+' => '', - 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]". Expecting an array with the following keys: %2$s', + 'Class:AsyncTask' => 'Async. task', + 'Class:AsyncTask+' => '', + 'Class:AsyncTask/Attribute:created' => 'Created', + 'Class:AsyncTask/Attribute:created+' => '', + 'Class:AsyncTask/Attribute:started' => 'Started', + 'Class:AsyncTask/Attribute:started+' => '', + 'Class:AsyncTask/Attribute:planned' => 'Planned', + 'Class:AsyncTask/Attribute:planned+' => '', + 'Class:AsyncTask/Attribute:event_id' => 'Event', + 'Class:AsyncTask/Attribute:event_id+' => '', + 'Class:AsyncTask/Attribute:finalclass' => 'Final class', + 'Class:AsyncTask/Attribute:finalclass+' => '', + 'Class:AsyncTask/Attribute:status' => 'Status', + 'Class:AsyncTask/Attribute:status+' => '', + 'Class:AsyncTask/Attribute:remaining_retries' => 'Remaining retries', + 'Class:AsyncTask/Attribute:remaining_retries+' => '', + 'Class:AsyncTask/Attribute:last_error_code' => 'Last error code', + 'Class:AsyncTask/Attribute:last_error_code+' => '', + 'Class:AsyncTask/Attribute:last_error' => 'Last error', + 'Class:AsyncTask/Attribute:last_error+' => '', + 'Class:AsyncTask/Attribute:last_attempt' => 'Last attempt', + 'Class:AsyncTask/Attribute:last_attempt+' => '', + 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]". Expecting an array with the following keys: %2$s', 'Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]": unexpected key "%2$s". Expecting only the following keys: %3$s', ]); @@ -1284,7 +1285,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:AbstractResource' => 'Abstract Resource', + 'Class:AbstractResource' => 'Abstract Resource', 'Class:AbstractResource+' => '', ]); @@ -1293,7 +1294,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceAdminMenu' => 'Resource Admin Menu', + 'Class:ResourceAdminMenu' => 'Resource Admin Menu', 'Class:ResourceAdminMenu+' => '', ]); @@ -1302,7 +1303,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceRunQueriesMenu' => 'Resource Run Queries Menu', + 'Class:ResourceRunQueriesMenu' => 'Resource Run Queries Menu', 'Class:ResourceRunQueriesMenu+' => '', ]); @@ -1311,6 +1312,6 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceSystemMenu' => 'Resource System Menu', + 'Class:ResourceSystemMenu' => 'Resource System Menu', 'Class:ResourceSystemMenu+' => '', ]); diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index dd1f849806..1541db4cc5 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -162,13 +162,14 @@ 'Core:Context=GUI:Portal' => 'Portal', 'Core:Context=GUI:Portal+' => 'GUI:Portal', - 'Core:GetQuota:Error' => 'Erreur lors de la récupération du quota des %1$s', - 'Core:ConsoleUsers' => 'utilisateurs console', - 'Core:DisabledUsers' => 'utilisateurs désactivés', - 'Core:PortalUsers' => 'utilisateurs du portail', - 'Core:BusinessPartnerUser' => 'utilisateurs partenaires business', - 'Core:ReadOnlyUsers' => 'utilisateurs en lecture seule', - 'Core:ApplicationUsers' => 'utilisateurs applicatifs', + 'Core:GetCountingUsers:Error' => 'Erreur lors du comptage des %1$s', + 'Core:CountingUsers:ConsoleUsers' => 'utilisateurs console', + 'Core:CountingUsers:DisabledUsers' => 'utilisateurs désactivés', + 'Core:CountingUsers:PortalUsers' => 'utilisateurs du portail', + 'Core:CountingUsers:BusinessPartnerUser' => 'utilisateurs partenaires business', + 'Core:CountingUsers:ReadOnlyUsers' => 'utilisateurs en lecture seule', + 'Core:CountingUsers:ApplicationUsers' => 'utilisateurs applicatifs', + 'Core:CountingUsers:AllUsers' => 'tous les utilisateurs', ]); ////////////////////////////////////////////////////////////////////// diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index 75bb20c680..0ad0d9ddc2 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -20,7 +20,7 @@ use UserRights; /** - * + * @description Repository that aims to count users based on their type */ class ITopUserCountingRepository { @@ -37,7 +37,7 @@ class ITopUserCountingRepository * @throws MySQLException * @throws Exception */ - public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProfiles = [], bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array + public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProfiles = ['Portal user'], bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array { $sExcludedUsers = $this->ArrayToOQLStringParameter($aExcludedUsers); $sExcludedProfiles = $this->ArrayToOQLStringParameter($aExcludedProfiles); @@ -57,66 +57,33 @@ public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProf try { $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLUserConsole) : DBObjectSearch::FromOQL($sOQLUserConsole); } catch (Exception $e) { - IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ConsoleUsers'))); + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ConsoleUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ConsoleUsers')).'.'); } $aConsoleUsers = $this->GetUsersFromFilter($oFilter); - $aPortalUsers = $this->GetPortalUsers(); $aReadOnlyUsers = $this->GetReadOnlyUsers(); - - return array_diff($aConsoleUsers, $aPortalUsers, $aReadOnlyUsers); - } - - private function ArrayToOQLStringParameter(array $aValues): string - { - $aQuotedValues = []; - foreach ($aValues as $value) { - $value = trim((string) $value); - if ($value === '') { - continue; - } - $aQuotedValues[] = "'".addslashes($value)."'"; - } - - return empty($aQuotedValues) ? "''" : implode(', ', $aQuotedValues); - } - - /** - * @throws CoreUnexpectedValue - * @throws CoreException - * @throws MySQLException - */ - public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array - { - $aUsers = []; - if (is_null($oFilter)) { - return $aUsers; - } - $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); - while ($oUser = $oSet->Fetch()) { - $aUsers[] = $oUser; - } - - return $aUsers; + return array_diff($aConsoleUsers, $aReadOnlyUsers); } /** * @throws Exception */ - public function GetPortalUsers(bool $bAllData = true): array + public function GetPortalUsers(bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array { + $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); + $sOQLPortalUser = " SELECT User AS u JOIN URP_UserProfile AS uup ON uup.userid = u.id JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.id = '2' AND u.status != 'disabled' "; + WHERE up.id = '2' AND u.status != 'disabled' AND u.finalclass NOT IN ($sExcludedFinalClasses)"; try { $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); } catch (Exception $e) { - IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:PortalUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:PortalUsers')).'.'); } return $this->GetUsersFromFilter($oFilter); @@ -143,13 +110,43 @@ public function GetReadOnlyUsers(): array } } - // remove portal users - $aPortalUsers = $this->GetPortalUsers(); - $aReadOnlyUsers = array_diff($aReadOnlyUsers, $aPortalUsers); - // remove disabled users - $aDisabledUsers = $this->GetDisabledUsers(); + $aUserToken = $this->GetApplicationUsers(); + return array_diff($aReadOnlyUsers, $aUserToken); + } - return array_diff($aReadOnlyUsers, $aDisabledUsers); + /** + * @throws Exception + */ + public function GetDisabledUsers(bool $bAllData = true): array + { + $sOQLDisabledUser = " + SELECT User AS u + WHERE u.status = 'disabled' + "; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); + } catch (Exception $e) { + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:DisabledUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:DisabledUsers')).'.'); + } + + return $this->GetUsersFromFilter($oFilter); + } + + /** + * @throws Exception + */ + public function GetApplicationUsers(bool $bAllData = true): array + { + $sOQLApplicationUser = 'SELECT UserToken'; + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); + } catch (Exception $e) { + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ApplicationUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ApplicationUsers')).'.'); + } + + return $this->GetUsersFromFilter($oFilter); } /** @@ -162,8 +159,8 @@ public function GetAllUsers(bool $bAllData = true): array try { $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOqlUser) : DBObjectSearch::FromOQL($sOqlUser); } catch (Exception $e) { - IssueLog::Error('combodo-users-quota-slave/GetUsersNotInQuota : '.$e->getMessage(), 'combodo-users-quota'); - throw new Exception(Dict::S('CombodoUserQuota:Error')); + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:AllUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:AllUsers')).'.'); } return $this->GetUsersFromFilter($oFilter); @@ -179,28 +176,11 @@ public function GetAllUsers(bool $bAllData = true): array */ private function IsUserReadOnly(User $oUser, string $sClassCategory): bool { - if ($oUser->Get('status') == 'disabled') { - return false; - } - - // check if user is a portal user - $oProfileLinks = $oUser->Get('profile_list'); - while ($oLink = $oProfileLinks->Fetch()) { - $iProfileId = $oLink->Get('profileid'); - if (!$iProfileId) { - continue; - } - $oProfile = MetaModel::GetObject('URP_Profiles', $iProfileId, false); - if ($oProfile && $oProfile->Get('name') === PORTAL_PROFILE_NAME) { - return false; - } - } - // login (mandatory to compute rights) UserRights::Login($oUser->GetName()); foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { - // no need to check stimuli for now since users can't execute stimulus without UR_ACTION_MODIFY + // no need to check stimuli for now since users can't execute any without UR_ACTION_MODIFY if ( UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || @@ -216,37 +196,36 @@ private function IsUserReadOnly(User $oUser, string $sClassCategory): bool } /** - * @throws Exception + * @throws CoreUnexpectedValue + * @throws CoreException + * @throws MySQLException */ - public function GetDisabledUsers(bool $bAllData = true): array + public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array { - $sOQLDisabledUser = " - SELECT User AS u - WHERE u.status = 'disabled' - "; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); - } catch (Exception $e) { - IssueLog::Error('Core:GetDisabledUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:DisabledUsers'))); + $aUsers = []; + if (is_null($oFilter)) { + return $aUsers; + } + $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); + while ($oUser = $oSet->Fetch()) { + $aUsers[] = $oUser; } - return $this->GetUsersFromFilter($oFilter); + return $aUsers; } - /** - * @throws Exception - */ - public function GetApplicationUsers(bool $bAllData = true): array + private function ArrayToOQLStringParameter(array $aValues): string { - $sOQLApplicationUser = 'SELECT UserToken'; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); - } catch (Exception $e) { - IssueLog::Error('Core:GetConsoleUsersQuota:Error : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetQuota:Error', Dict::S('Core:ApplicationUsers'))); + $aQuotedValues = []; + foreach ($aValues as $value) { + $value = trim((string) $value); + if ($value === '') { + continue; + } + $aQuotedValues[] = "'".addslashes($value)."'"; } - return $this->GetUsersFromFilter($oFilter); + return empty($aQuotedValues) ? "''" : implode(', ', $aQuotedValues); } + } diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 7a58d0394f..88d4ab0a18 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1466,6 +1466,31 @@ protected function GivenUserWithContactInDB(string $sLogin, string $sProfileId, ]); } + /** + * @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'] diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 8ea985757c..a603327009 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -12,6 +12,10 @@ protected function setUp(): void { parent::setUp(); $this->CreateReadOnlyUsers(); + $this->CreateDisabledUsers(); + $this->CreatePortalUsers(); + $this->CreateTokenUsers(); + $this->CreateConsoleUsers(); } @@ -20,18 +24,48 @@ private function CreateReadOnlyUsers() $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly']); $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly']); $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Service Catalog ReadOnly']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Portal user']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly', 'Portal user']); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Service Catalog ReadOnly', 'Portal user']); $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly']); $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly', 'Service Catalog ReadOnly']); $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly', 'Service Catalog ReadOnly']); } - private function CreateDisabledUser() + private function CreateDisabledUsers() { - $sUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager']); - // get user by login - $oUser = \MetaModel::GetObjectByName('User', $sUser); + $iDisabledUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], false); + $oUser = \MetaModel::GetObject('User', $iDisabledUser); $oUser->Set('status', 'disabled'); $oUser->DBUpdate(); + + $iDisabledReadOnlyUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly'], false); + $oUser = \MetaModel::GetObject('User', $iDisabledReadOnlyUser); + $oUser->Set('status', 'disabled'); + $oUser->DBUpdate(); + } + + private function CreatePortalUsers() + { + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], false); + + $iDisabledPortalUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], false); + $oUser = \MetaModel::GetObject('User', $iDisabledPortalUser); + $oUser->Set('status', 'disabled'); + $oUser->DBUpdate(); + } + + private function CreateTokenUsers() + { + $this->GivenTokenUserInDB(['Configuration Manager'], false); + $this->GivenTokenUserInDB(['Portal user'], false); + $this->GivenTokenUserInDB(['Configuration ReadOnly'], false); + } + + private function CreateConsoleUsers() + { + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Administrator'], false); } /** @@ -110,5 +144,4 @@ public function testAllCountedUsersAreUsersObjects() } } - } From c38ef99771c6d97b1f6ed02243ff6f2714e71129 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Tue, 9 Jun 2026 17:47:56 +0200 Subject: [PATCH 09/22] With business portal users --- sources/Users/ITopUserCountingRepository.php | 25 ++++++++++- .../src/BaseTestCase/ItopDataTestCase.php | 1 + .../Users/ITopUserCountingRepositoryTest.php | 41 ++++++++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index 0ad0d9ddc2..e2356b9047 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -37,7 +37,7 @@ class ITopUserCountingRepository * @throws MySQLException * @throws Exception */ - public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProfiles = ['Portal user'], bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array + public function GetConsoleUsers(array $aExcludedUsers = [], array $aExcludedProfiles = ['Portal user', 'Business partner user'], bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array { $sExcludedUsers = $this->ArrayToOQLStringParameter($aExcludedUsers); $sExcludedProfiles = $this->ArrayToOQLStringParameter($aExcludedProfiles); @@ -89,6 +89,29 @@ public function GetPortalUsers(bool $bAllData = true, array $aExcludedFinalClass return $this->GetUsersFromFilter($oFilter); } + /** + * @throws Exception + */ + public function GetBusinessPartnerUsers(bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array + { + $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); + + $sOQLPortalUser = " + SELECT User AS u + JOIN URP_UserProfile AS uup ON uup.userid = u.id + JOIN URP_Profiles AS up ON uup.profileid = up.id + WHERE up.id = '40' AND u.status != 'disabled' AND u.finalclass NOT IN ($sExcludedFinalClasses)"; + + try { + $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); + } catch (Exception $e) { + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUser')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUser')).'.'); + } + + return $this->GetUsersFromFilter($oFilter); + } + /** * @throws DictExceptionMissingString * @throws CoreException diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 88d4ab0a18..82e0e129f9 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -100,6 +100,7 @@ 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, diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index a603327009..71cda244d4 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -16,6 +16,7 @@ protected function setUp(): void $this->CreatePortalUsers(); $this->CreateTokenUsers(); $this->CreateConsoleUsers(); + $this->CreateBusinessPartnerUser(); } @@ -68,6 +69,12 @@ private function CreateConsoleUsers() $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Administrator'], false); } + private function CreateBusinessPartnerUser() + { + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], false); + } + /** * @throws \CoreUnexpectedValue * @throws \DictExceptionMissingString @@ -85,6 +92,7 @@ public function testNotDuplicateInDifferentCountsCategories(): void 'disabled' => $oITopUserRepository->GetDisabledUsers(), 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), 'application' => $oITopUserRepository->GetApplicationUsers(), + 'businesspartner' => $oITopUserRepository->GetBusinessPartnerUsers(), ]; $aCountedUserFormated = []; @@ -119,12 +127,40 @@ public function testAllUsersAreCounted() $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); + $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); - $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); $aAllUsersFromOQL = $oITopUserRepository->GetAllUsers(); - $this->assertEmpty(array_merge(array_diff($aAllUsersFromMergedCounts, $aAllUsersFromOQL), array_diff($aAllUsersFromOQL, $aAllUsersFromMergedCounts))); + $this->assertEmpty( // asserts that all users are in both arrays + array_merge( + array_diff( + $aAllUsersFromMergedCounts, + $aAllUsersFromOQL + ), + array_diff( + $aAllUsersFromOQL, + $aAllUsersFromMergedCounts + ) + ) + ); + + // check for cardinality + $aMergedCountingUsersIds = []; + foreach ($aAllUsersFromMergedCounts as $oUser) { + $aMergedCountingUsersIds[] = (int) $oUser->GetKey(); + } + + $aOqlUsersIds = []; + foreach ($aAllUsersFromOQL as $oUser) { + $aOqlUsersIds[] = (int) $oUser->GetKey(); + } + + sort($aMergedCountingUsersIds, SORT_NUMERIC); + sort($aOqlUsersIds, SORT_NUMERIC); + + $this->assertSame($aOqlUsersIds, $aMergedCountingUsersIds); } public function testAllCountedUsersAreUsersObjects() @@ -136,6 +172,7 @@ public function testAllCountedUsersAreUsersObjects() $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); + $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); From 38e4a126569126edf7c8f02459303d021e568fcc Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 12:13:52 +0200 Subject: [PATCH 10/22] Add more unit tests --- .../Users/ITopUserCountingRepositoryTest.php | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 71cda244d4..21a4b7429e 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -174,11 +174,57 @@ public function testAllCountedUsersAreUsersObjects() $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); - $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers); + $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); foreach ($aCountedUsers as $oUser) { $this->assertInstanceOf(User::class, $oUser); } + } + + /** + * @param array $aProfilesList + * @param $sExpectedCategorie + * + * @return void + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \DictExceptionMissingString + * @throws \MySQLException + * @dataProvider RealUseCasesDataProvider + */ + public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) + { + $iUser = $this->GivenUserInDB('<_ç"éue"ç_u', $aProfilesList, false); + $oUser = \MetaModel::GetObject('User', $iUser); + $oITopUserRepository = new ITopUserCountingRepository(); + $aUsers = match ($sExpectedCategorie) { + 'console' => $oITopUserRepository->GetConsoleUsers(), + 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), + 'disabled' => $oITopUserRepository->GetDisabledUsers(), + 'portal' => $oITopUserRepository->GetPortalUsers(), + 'application' => $oITopUserRepository->GetApplicationUsers(), + 'businesspartner' => $oITopUserRepository->GetBusinessPartnerUsers(), + default => throw new \InvalidArgumentException(sprintf('Unexpected category %s', $sExpectedCategorie)), + }; + $bUserInCorrectCategory = false; + foreach ($aUsers as $oUserInCategory) { + if ($oUserInCategory->GetKey() === $oUser->GetKey()) { + $bUserInCorrectCategory = true; + } + } + $this->assertTrue($bUserInCorrectCategory, 'User with profiles '.implode(', ', $aProfilesList).' should be counted as '.$sExpectedCategorie.' user'); + } + + public function RealUseCasesDataProvider() + { + return [ + [['Support Agent', 'Configuration ReadOnly'], 'console'], + [['Configuration ReadOnly', 'Service Catalog ReadOnly'], 'readonly'], + [['Portal user', 'Service Catalog ReadOnly'], 'portal'], + [['Support Agent', 'Portal user'], 'portal'], + [['Configuration Manager', 'Ticket ReadOnly'], 'console'], + ]; } } From a985504d92d6fec4141dceb0610e2372357fedda Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 12:22:14 +0200 Subject: [PATCH 11/22] Improve test readability --- .../Users/ITopUserCountingRepositoryTest.php | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 21a4b7429e..e7b16946f2 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -220,11 +220,26 @@ public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) public function RealUseCasesDataProvider() { return [ - [['Support Agent', 'Configuration ReadOnly'], 'console'], - [['Configuration ReadOnly', 'Service Catalog ReadOnly'], 'readonly'], - [['Portal user', 'Service Catalog ReadOnly'], 'portal'], - [['Support Agent', 'Portal user'], 'portal'], - [['Configuration Manager', 'Ticket ReadOnly'], 'console'], + [ + 'profiles' => ['Support Agent', 'Configuration ReadOnly'], + 'expected category' => 'console', + ], + [ + 'profiles' => ['Configuration ReadOnly', 'Service Catalog ReadOnly'], + 'expected category' => 'readonly', + ], + [[ + 'profiles' => 'Portal user', 'Service Catalog ReadOnly'], + 'expected category' => 'portal', + ], + [ + 'profiles' => ['Support Agent', 'Portal user'], + 'expected category' => 'portal', + ], + [ + 'profiles' => ['Configuration Manager', 'Ticket ReadOnly'], + 'expected category' => 'console', + ], ]; } } From c4c21964c035a0164106e3462f751d71f02b72b7 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 12:39:39 +0200 Subject: [PATCH 12/22] Adapt test after test method signature changed (on rebase). --- .../Users/ITopUserCountingRepositoryTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index e7b16946f2..88417e52b7 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -35,12 +35,12 @@ private function CreateReadOnlyUsers() private function CreateDisabledUsers() { - $iDisabledUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], false); + $iDisabledUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], bReturnLogin: false); $oUser = \MetaModel::GetObject('User', $iDisabledUser); $oUser->Set('status', 'disabled'); $oUser->DBUpdate(); - $iDisabledReadOnlyUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly'], false); + $iDisabledReadOnlyUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly'], bReturnLogin: false); $oUser = \MetaModel::GetObject('User', $iDisabledReadOnlyUser); $oUser->Set('status', 'disabled'); $oUser->DBUpdate(); @@ -50,7 +50,7 @@ private function CreatePortalUsers() { $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], false); - $iDisabledPortalUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], false); + $iDisabledPortalUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], bReturnLogin: false); $oUser = \MetaModel::GetObject('User', $iDisabledPortalUser); $oUser->Set('status', 'disabled'); $oUser->DBUpdate(); @@ -65,14 +65,14 @@ private function CreateTokenUsers() private function CreateConsoleUsers() { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], false); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Administrator'], false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], bReturnLogin: false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Administrator'], bReturnLogin: false); } private function CreateBusinessPartnerUser() { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], false); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], bReturnLogin: false); + $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], bReturnLogin: false); } /** @@ -195,7 +195,7 @@ public function testAllCountedUsersAreUsersObjects() */ public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) { - $iUser = $this->GivenUserInDB('<_ç"éue"ç_u', $aProfilesList, false); + $iUser = $this->GivenUserInDB('<_ç"éue"ç_u', $aProfilesList, bReturnLogin: false); $oUser = \MetaModel::GetObject('User', $iUser); $oITopUserRepository = new ITopUserCountingRepository(); $aUsers = match ($sExpectedCategorie) { From 286671d4091e39e964a0f9c6068dde266388a4fc Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 13:58:04 +0200 Subject: [PATCH 13/22] CS fixer --- .../unitary-tests/core/UserRightsTest.php | 288 +++++++++--------- 1 file changed, 149 insertions(+), 139 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php index d0bde71885..ec47012dba 100644 --- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php +++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php @@ -29,7 +29,6 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use CoreCannotSaveObjectException; -use CoreException; use DBObject; use DBObjectSearch; use DBObjectSet; @@ -47,13 +46,6 @@ */ class UserRightsTest extends ItopDataTestCase { - public function setUp(): void - { - parent::setUp(); - - utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); - } - public static $aClasses = [ 'FunctionalCI' => ['class' => 'FunctionalCI', 'attcode' => 'name'], 'URP_UserProfile' => ['class' => 'URP_UserProfile', 'attcode' => 'reason'], @@ -62,39 +54,11 @@ public function setUp(): void 'ModuleInstallation' => ['class' => 'ModuleInstallation', 'attcode' => 'name'], ]; - /** - * @param string $sLoginPrefix - * @param int $iProfileId initial profile - * - * @return \DBObject - * @throws \CoreException - * @throws \Exception - */ - protected function CreateUniqueUserAndLogin(string $sLoginPrefix, int $iProfileId): DBObject + public function setUp(): void { - static $iCount = 0; - $sLogin = $sLoginPrefix.$iCount; - $iCount++; - - $oUser = self::CreateUser($sLogin, $iProfileId); - $_SESSION = []; - UserRights::Login($sLogin); - return $oUser; - } + parent::setUp(); - protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject - { - $oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class)); - foreach ($aProfileIds as $iProfileId) { - $oProfiles->AddItem(MetaModel::NewObject('URP_UserProfile', ['profileid' => $iProfileId, 'reason' => 'UNIT Tests'])); - } - $oUser = MetaModel::NewObject('UserLocal', [ - 'login' => $sLogin, - 'password' => 'Password1!', - 'expiration' => UserLocal::EXPIRE_NEVER, - 'profile_list' => $oProfiles, - ]); - return $oUser; + utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); } /** @@ -137,87 +101,29 @@ public function testReadOnlyUser(array $aProfileIds, array $aShouldBeAllowedToSe $bIsAllowedEditing = (bool)UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY); - $this->assertSame($bIsAllowedEditing, $bShouldBeAllowedToEdit, + $this->assertSame( + $bIsAllowedEditing, + $bShouldBeAllowedToEdit, "User with profiles ".implode(',', $aProfileIds)." should ".($bShouldBeAllowedToEdit ? "" : "NOT ")."be allowed to edit class $sClass" ); } } - 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' => [] - ], - ]; + + protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject + { + $oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class)); + foreach ($aProfileIds as $iProfileId) { + $oProfiles->AddItem(MetaModel::NewObject('URP_UserProfile', ['profileid' => $iProfileId, 'reason' => 'UNIT Tests'])); + } + $oUser = MetaModel::NewObject('UserLocal', [ + 'login' => $sLogin, + 'password' => 'Password1!', + 'expiration' => UserLocal::EXPIRE_NEVER, + 'profile_list' => $oProfiles, + ]); + return $oUser; } + public function testIsLoggedIn() { $this->assertFalse(UserRights::IsLoggedIn()); @@ -303,19 +209,26 @@ public function testIsActionAllowed(int $iProfileId, array $aClassActionResult) $this->assertEquals($aClassActionResult['res'], $bRes); } - /* - * FunctionalCI => bizmodel searchable - * UserRequest => bizmodel searchable requestmgmt - * URP_UserProfile => addon/userrights - * UserLocal => addon/authentication - * ModuleInstallation => core view_in_gui - * - * Profiles: - * 1 - Administrator - * 2 - User Portal - * 3 - Configuration manager + /** + * @param string $sLoginPrefix + * @param int $iProfileId initial profile * + * @return \DBObject + * @throws \CoreException + * @throws \Exception */ + protected function CreateUniqueUserAndLogin(string $sLoginPrefix, int $iProfileId): DBObject + { + static $iCount = 0; + $sLogin = $sLoginPrefix.$iCount; + $iCount++; + + $oUser = self::CreateUser($sLogin, $iProfileId); + $_SESSION = []; + UserRights::Login($sLogin); + return $oUser; + } + public function ActionAllowedProvider(): array { return [ @@ -363,6 +276,20 @@ public function ActionAllowedProvider(): array ]; } + /* + * FunctionalCI => bizmodel searchable + * UserRequest => bizmodel searchable requestmgmt + * URP_UserProfile => addon/userrights + * UserLocal => addon/authentication + * ModuleInstallation => core view_in_gui + * + * Profiles: + * 1 - Administrator + * 2 - User Portal + * 3 - Configuration manager + * + */ + /** Test IsActionAllowedOnAttribute * * @dataProvider ActionAllowedOnAttributeProvider @@ -382,14 +309,6 @@ public function testIsActionAllowedOnAttribute(int $iProfileId, array $aClassAct $this->assertEquals($aClassActionResult['res'], $bRes); } - /* - * FunctionalCI => bizmodel searchable - * UserRequest => bizmodel searchable requestmgmt - * URP_UserProfile => addon/userrights grant_by_profile - * UserLocal => addon/authentication grant_by_profile - * ModuleInstallation => core view_in_gui - * - */ public function ActionAllowedOnAttributeProvider(): array { return [ @@ -416,6 +335,15 @@ public function ActionAllowedOnAttributeProvider(): array ]; } + /* + * FunctionalCI => bizmodel searchable + * UserRequest => bizmodel searchable requestmgmt + * URP_UserProfile => addon/userrights grant_by_profile + * UserLocal => addon/authentication grant_by_profile + * ModuleInstallation => core view_in_gui + * + */ + /** * @dataProvider UserCannotLoseConsoleAccessProvider * @@ -559,6 +487,7 @@ public function testPrivilegedUsersMustHaveBackofficeAccess(int $iProfileId) $oUser->DBInsert(); } + public function PrivilegedUsersMustHaveBackofficeAccessProvider(): array { return [ @@ -568,6 +497,7 @@ public function PrivilegedUsersMustHaveBackofficeAccessProvider(): array ]; } + public function testNonPrivilegedUsersCanBeDeniedFromBackoffice() { $oUser = $this->GivenUserWithProfiles('test1', [5, 2]); @@ -603,6 +533,7 @@ public function NonAdminCanListOwnProfilesProvider(): array 'with Admins hidden' => [true], ]; } + /** * @dataProvider NonAdminCannotListAdminProfilesProvider */ @@ -649,6 +580,12 @@ public function testFindUser_ExistingInternalUser() ); } + public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); + } + public function testFindUser_ExistingExternalUser() { $sLogin = 'AnExternalUser'.uniqid(); @@ -684,14 +621,87 @@ public function testFindUser_UnknownLogin() ); } - public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) - { - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); - } public function FindUserAndAssertItWasNotFound($sLogin) { $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' => [], + ], + ]; + } } From 0d5bfb97cefbe0dce2b765335a788ebe2f83d636 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 15:19:58 +0200 Subject: [PATCH 14/22] Revert format --- dictionaries/en.dictionary.itop.core.php | 1075 +++++++++++----------- 1 file changed, 537 insertions(+), 538 deletions(-) diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index b7ecb91796..178c752bd9 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -24,125 +24,125 @@ Dict::Add('EN US', 'English', 'English', [ 'Core:DeletedObjectLabel' => '%1s (deleted)', - 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)', + 'Core:DeletedObjectTip' => 'The object has been deleted on %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'Object not found (class: %1$s, id: %2$d)', - 'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.', + 'Core:UnknownObjectTip' => 'The object could not be found. It may have been deleted some time ago and the log has been purged since.', 'Core:UniquenessDefaultError' => 'Uniqueness rule \'%1$s\' in error', - 'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s', - 'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s', + 'Core:CheckConsistencyError' => 'Consistency rules not followed: %1$s', + 'Core:CheckValueError' => 'Unexpected value for attribute \'%1$s\' (%2$s) : %3$s', - 'Core:AttributeLinkedSet' => 'Array of objects', + 'Core:AttributeLinkedSet' => 'Array of objects', 'Core:AttributeLinkedSet+' => 'Any kind of objects of the same class or subclass', 'Core:AttributeLinkedSetDuplicatesFound' => 'Duplicates in the \'%1$s\' field : %2$s', - 'Core:AttributeDashboard' => 'Dashboard', + 'Core:AttributeDashboard' => 'Dashboard', 'Core:AttributeDashboard+' => '', - 'Core:AttributePhoneNumber' => 'Phone number', + 'Core:AttributePhoneNumber' => 'Phone number', 'Core:AttributePhoneNumber+' => '', - 'Core:AttributeObsolescenceDate' => 'Obsolescence date', + 'Core:AttributeObsolescenceDate' => 'Obsolescence date', 'Core:AttributeObsolescenceDate+' => '', - 'Core:AttributeTagSet' => 'List of tags', - 'Core:AttributeTagSet+' => '', - 'Core:AttributeSet:placeholder' => 'click to add', - 'Core:Placeholder:CannotBeResolved' => '(%1$s : cannot be resolved)', - 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)', - 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)', + 'Core:AttributeTagSet' => 'List of tags', + 'Core:AttributeTagSet+' => '', + 'Core:AttributeSet:placeholder' => 'click to add', + 'Core:Placeholder:CannotBeResolved' => '(%1$s : cannot be resolved)', + 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromClass' => '%1$s (%2$s)', + 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromOneChildClass' => '%1$s (%2$s from %3$s)', 'Core:AttributeClassAttCodeSet:ItemLabel:AttributeFromSeveralChildClasses' => '%1$s (%2$s from child classes)', - 'Core:AttributeCaseLog' => 'Log', + 'Core:AttributeCaseLog' => 'Log', 'Core:AttributeCaseLog+' => '', - 'Core:AttributeMetaEnum' => 'Computed enum', + 'Core:AttributeMetaEnum' => 'Computed enum', 'Core:AttributeMetaEnum+' => '', - 'Core:AttributeLinkedSetIndirect' => 'Array of objects (N-N)', + 'Core:AttributeLinkedSetIndirect' => 'Array of objects (N-N)', 'Core:AttributeLinkedSetIndirect+' => 'Any kind of objects [subclass] of the same class', - 'Core:AttributeInteger' => 'Integer', + 'Core:AttributeInteger' => 'Integer', 'Core:AttributeInteger+' => 'Numeric value (could be negative)', - 'Core:AttributeDecimal' => 'Decimal', + 'Core:AttributeDecimal' => 'Decimal', 'Core:AttributeDecimal+' => 'Decimal value (could be negative)', - 'Core:AttributeBoolean' => 'Boolean', - 'Core:AttributeBoolean+' => '', + 'Core:AttributeBoolean' => 'Boolean', + 'Core:AttributeBoolean+' => '', 'Core:AttributeBoolean/Value:null' => '', - 'Core:AttributeBoolean/Value:yes' => 'Yes', - 'Core:AttributeBoolean/Value:no' => 'No', + 'Core:AttributeBoolean/Value:yes' => 'Yes', + 'Core:AttributeBoolean/Value:no' => 'No', - 'Core:AttributeArchiveFlag' => 'Archive flag', - 'Core:AttributeArchiveFlag/Value:yes' => 'Yes', + 'Core:AttributeArchiveFlag' => 'Archive flag', + 'Core:AttributeArchiveFlag/Value:yes' => 'Yes', 'Core:AttributeArchiveFlag/Value:yes+' => 'This object is visible only in archive mode', - 'Core:AttributeArchiveFlag/Value:no' => 'No', - 'Core:AttributeArchiveFlag/Label' => 'Archived', - 'Core:AttributeArchiveFlag/Label+' => '', - 'Core:AttributeArchiveDate/Label' => 'Archive date', - 'Core:AttributeArchiveDate/Label+' => '', - - 'Core:AttributeObsolescenceFlag' => 'Obsolescence flag', - 'Core:AttributeObsolescenceFlag/Value:yes' => 'Yes', + 'Core:AttributeArchiveFlag/Value:no' => 'No', + 'Core:AttributeArchiveFlag/Label' => 'Archived', + 'Core:AttributeArchiveFlag/Label+' => '', + 'Core:AttributeArchiveDate/Label' => 'Archive date', + 'Core:AttributeArchiveDate/Label+' => '', + + 'Core:AttributeObsolescenceFlag' => 'Obsolescence flag', + 'Core:AttributeObsolescenceFlag/Value:yes' => 'Yes', 'Core:AttributeObsolescenceFlag/Value:yes+' => 'This object is excluded from the impact analysis, and hidden from search results', - 'Core:AttributeObsolescenceFlag/Value:no' => 'No', - 'Core:AttributeObsolescenceFlag/Label' => 'Obsolete', - 'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes', - 'Core:AttributeObsolescenceDate/Label' => 'Obsolescence date', - 'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete', + 'Core:AttributeObsolescenceFlag/Value:no' => 'No', + 'Core:AttributeObsolescenceFlag/Label' => 'Obsolete', + 'Core:AttributeObsolescenceFlag/Label+' => 'Computed dynamically on other attributes', + 'Core:AttributeObsolescenceDate/Label' => 'Obsolescence date', + 'Core:AttributeObsolescenceDate/Label+' => 'Approximative date at which the object has been considered obsolete', - 'Core:AttributeString' => 'String', + 'Core:AttributeString' => 'String', 'Core:AttributeString+' => 'Alphanumeric string', - 'Core:AttributeClass' => 'Class', + 'Core:AttributeClass' => 'Class', 'Core:AttributeClass+' => '', - 'Core:AttributeApplicationLanguage' => 'User language', + 'Core:AttributeApplicationLanguage' => 'User language', 'Core:AttributeApplicationLanguage+' => 'Language and country (EN US)', - 'Core:AttributeFinalClass' => 'Class (auto)', + 'Core:AttributeFinalClass' => 'Class (auto)', 'Core:AttributeFinalClass+' => 'Real class of the object (automatically created by the core)', - 'Core:AttributePassword' => 'Password', + 'Core:AttributePassword' => 'Password', 'Core:AttributePassword+' => 'Password of an external device', - 'Core:AttributeEncryptedString' => 'Encrypted string', - 'Core:AttributeEncryptedString+' => 'String encrypted with a local key', - 'Core:AttributeEncryptUnknownLibrary' => 'Encryption library specified (%1$s) unknown', + 'Core:AttributeEncryptedString' => 'Encrypted string', + 'Core:AttributeEncryptedString+' => 'String encrypted with a local key', + 'Core:AttributeEncryptUnknownLibrary' => 'Encryption library specified (%1$s) unknown', 'Core:AttributeEncryptFailedToDecrypt' => '** decryption error **', - 'Core:AttributeText' => 'Text', + 'Core:AttributeText' => 'Text', 'Core:AttributeText+' => 'Multiline character string', - 'Core:AttributeHTML' => 'HTML', + 'Core:AttributeHTML' => 'HTML', 'Core:AttributeHTML+' => 'HTML string', - 'Core:AttributeEmailAddress' => 'Email address', + 'Core:AttributeEmailAddress' => 'Email address', 'Core:AttributeEmailAddress+' => 'Email address', - 'Core:AttributeIPAddress' => 'IP address', + 'Core:AttributeIPAddress' => 'IP address', 'Core:AttributeIPAddress+' => 'IP address', - 'Core:AttributeOQL' => 'OQL', + 'Core:AttributeOQL' => 'OQL', 'Core:AttributeOQL+' => 'Object Query Language expression', - 'Core:AttributeEnum' => 'Enum', + 'Core:AttributeEnum' => 'Enum', 'Core:AttributeEnum+' => 'List of predefined alphanumeric strings', - 'Core:AttributeTemplateString' => 'Template string', + 'Core:AttributeTemplateString' => 'Template string', 'Core:AttributeTemplateString+' => 'String containing placeholders', - 'Core:AttributeTemplateText' => 'Template text', + 'Core:AttributeTemplateText' => 'Template text', 'Core:AttributeTemplateText+' => 'Text containing placeholders', - 'Core:AttributeTemplateHTML' => 'Template HTML', + 'Core:AttributeTemplateHTML' => 'Template HTML', 'Core:AttributeTemplateHTML+' => 'HTML containing placeholders', - 'Core:AttributeDateTime' => 'Date/time', - 'Core:AttributeDateTime+' => 'Date and time (year-month-day hh:mm:ss)', + 'Core:AttributeDateTime' => 'Date/time', + 'Core:AttributeDateTime+' => 'Date and time (year-month-day hh:mm:ss)', 'Core:AttributeDateTime?SmartSearch' => '

Date format:
@@ -159,8 +159,8 @@ If the time is omitted, it defaults to 00:00:00

', - 'Core:AttributeDate' => 'Date', - 'Core:AttributeDate+' => 'Date (year-month-day)', + 'Core:AttributeDate' => 'Date', + 'Core:AttributeDate+' => 'Date (year-month-day)', 'Core:AttributeDate?SmartSearch' => '

Date format:
@@ -174,61 +174,60 @@ [date,date]

', - 'Core:AttributeDeadline' => 'Deadline', + 'Core:AttributeDeadline' => 'Deadline', 'Core:AttributeDeadline+' => 'Date, displayed relatively to the current time', - 'Core:AttributeExternalKey' => 'External key', + 'Core:AttributeExternalKey' => 'External key', 'Core:AttributeExternalKey+' => 'External (or foreign) key', - 'Core:AttributeHierarchicalKey' => 'Hierarchical Key', + 'Core:AttributeHierarchicalKey' => 'Hierarchical Key', 'Core:AttributeHierarchicalKey+' => 'External (or foreign) key to the parent', - 'Core:AttributeExternalField' => 'External field', + 'Core:AttributeExternalField' => 'External field', 'Core:AttributeExternalField+' => 'Field mapped to an external key', - 'Core:AttributeURL' => 'URL', + 'Core:AttributeURL' => 'URL', 'Core:AttributeURL+' => 'Absolute or relative URL as a text string', - 'Core:AttributeBlob' => 'Blob', + 'Core:AttributeBlob' => 'Blob', 'Core:AttributeBlob+' => 'Any binary content (document)', - 'Core:AttributeOneWayPassword' => 'One way password', + 'Core:AttributeOneWayPassword' => 'One way password', 'Core:AttributeOneWayPassword+' => 'One way encrypted (hashed) password', - 'Core:AttributeTable' => 'Table', + 'Core:AttributeTable' => 'Table', 'Core:AttributeTable+' => 'Indexed array having two dimensions', - 'Core:AttributePropertySet' => 'Properties', + 'Core:AttributePropertySet' => 'Properties', 'Core:AttributePropertySet+' => 'List of untyped properties (name and value)', - 'Core:AttributeFriendlyName' => 'Friendly name', + 'Core:AttributeFriendlyName' => 'Friendly name', 'Core:AttributeFriendlyName+' => 'Attribute created automatically ; the friendly name is computed after several attributes', - 'Core:FriendlyName-Label' => 'Full name', + 'Core:FriendlyName-Label' => 'Full name', 'Core:FriendlyName-Description' => 'Full name', - 'Core:AttributeTag' => 'Tags', + 'Core:AttributeTag' => 'Tags', 'Core:AttributeTag+' => '', - 'Core:Context=REST/JSON' => 'REST', - 'Core:Context=Synchro' => 'Synchro', - 'Core:Context=Setup' => 'Setup', + 'Core:Context=REST/JSON' => 'REST', + 'Core:Context=Synchro' => 'Synchro', + 'Core:Context=Setup' => 'Setup', 'Core:Context=GUI:Console' => 'Console', - 'Core:Context=CRON' => 'cron', - 'Core:Context=GUI:Portal' => 'Portal', - - 'Core:GetCountingUsers:Error' => 'Error while counting %1$s', - 'Core:CountingUsers:ConsoleUsers' => 'console users', - 'Core:CountingUsers:DisabledUsers' => 'disabled users', - 'Core:CountingUsers:PortalUsers' => 'portal users', - 'Core:CountingUsers:BusinessPartnerUser' => 'business partner users', - 'Core:CountingUsers:ReadOnlyUsers' => 'read - only users', - 'Core:CountingUsers:ApplicationUsers' => 'application users', - 'Core:CountingUsers:AllUsers' => 'all users', + 'Core:Context=CRON' => 'cron', + 'Core:Context=GUI:Portal' => 'Portal', + + 'Core:GetCountingUsers:Error' => 'Error while getting %1$s quota', + 'Core:ConsoleUsers' => 'console users', + 'Core:DisabledUsers' => 'disabled users', + 'Core:PortalUsers' => 'portal users', + 'Core:BusinessPartnerUser' => 'business partner users', + 'Core:ReadOnlyUsers' => 'read-only users', + 'Core:ApplicationUsers' => 'application users', ]); ////////////////////////////////////////////////////////////////////// -// Classes in 'core / cmdb' +// Classes in 'core/cmdb' ////////////////////////////////////////////////////////////////////// // @@ -237,12 +236,12 @@ // Dict::Add('EN US', 'English', 'English', [ - 'class: CMDBChange' => 'Change', - 'class: CMDBChange+' => 'Changes tracking', - 'class: CMDBChange/Attribute:date' => 'date', - 'class: CMDBChange/Attribute:date + ' => 'date and time at which the changes have been recorded', - 'class: CMDBChange/Attribute:userinfo' => 'misc.info', - 'class: CMDBChange/Attribute:userinfo + ' => 'caller\'s defined information', + 'Class:CMDBChange' => 'Change', + 'Class:CMDBChange+' => 'Changes tracking', + 'Class:CMDBChange/Attribute:date' => 'date', + 'Class:CMDBChange/Attribute:date+' => 'date and time at which the changes have been recorded', + 'Class:CMDBChange/Attribute:userinfo' => 'misc. info', + 'Class:CMDBChange/Attribute:userinfo+' => 'caller\'s defined information', 'Class:CMDBChange/Attribute:origin/Value:interactive' => 'User interaction in the GUI', 'Class:CMDBChange/Attribute:origin/Value:csv-import.php' => 'CSV import script', 'Class:CMDBChange/Attribute:origin/Value:csv-interactive' => 'CSV import in the GUI', @@ -258,19 +257,19 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOp' => 'Change Operation', - 'Class:CMDBChangeOp+' => 'Change made by one person, at a single time, on a single object', - 'Class:CMDBChangeOp/Attribute:change' => 'change', - 'Class:CMDBChangeOp/Attribute:change+' => '', - 'Class:CMDBChangeOp/Attribute:date' => 'date', - 'Class:CMDBChangeOp/Attribute:date+' => 'date and time of the change', - 'Class:CMDBChangeOp/Attribute:userinfo' => 'user', - 'Class:CMDBChangeOp/Attribute:userinfo+' => 'who made this change', - 'Class:CMDBChangeOp/Attribute:objclass' => 'object class', - 'Class:CMDBChangeOp/Attribute:objclass+' => 'class name of the object on which the change was made', - 'Class:CMDBChangeOp/Attribute:objkey' => 'object id', - 'Class:CMDBChangeOp/Attribute:objkey+' => 'id of the object on which the change was made', - 'Class:CMDBChangeOp/Attribute:finalclass' => 'CMDBChangeOp sub-class', + 'Class:CMDBChangeOp' => 'Change Operation', + 'Class:CMDBChangeOp+' => 'Change made by one person, at a single time, on a single object', + 'Class:CMDBChangeOp/Attribute:change' => 'change', + 'Class:CMDBChangeOp/Attribute:change+' => '', + 'Class:CMDBChangeOp/Attribute:date' => 'date', + 'Class:CMDBChangeOp/Attribute:date+' => 'date and time of the change', + 'Class:CMDBChangeOp/Attribute:userinfo' => 'user', + 'Class:CMDBChangeOp/Attribute:userinfo+' => 'who made this change', + 'Class:CMDBChangeOp/Attribute:objclass' => 'object class', + 'Class:CMDBChangeOp/Attribute:objclass+' => 'class name of the object on which the change was made', + 'Class:CMDBChangeOp/Attribute:objkey' => 'object id', + 'Class:CMDBChangeOp/Attribute:objkey+' => 'id of the object on which the change was made', + 'Class:CMDBChangeOp/Attribute:finalclass' => 'CMDBChangeOp sub-class', 'Class:CMDBChangeOp/Attribute:finalclass+' => 'type of change which was performed', ]); @@ -279,7 +278,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpCreate' => 'object creation', + 'Class:CMDBChangeOpCreate' => 'object creation', 'Class:CMDBChangeOpCreate+' => 'Object creation tracking', ]); @@ -288,7 +287,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpDelete' => 'object deletion', + 'Class:CMDBChangeOpDelete' => 'object deletion', 'Class:CMDBChangeOpDelete+' => 'Object deletion tracking', ]); @@ -297,9 +296,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttribute' => 'object change', - 'Class:CMDBChangeOpSetAttribute+' => 'Object properties change tracking', - 'Class:CMDBChangeOpSetAttribute/Attribute:attcode' => 'Attribute', + 'Class:CMDBChangeOpSetAttribute' => 'object change', + 'Class:CMDBChangeOpSetAttribute+' => 'Object properties change tracking', + 'Class:CMDBChangeOpSetAttribute/Attribute:attcode' => 'Attribute', 'Class:CMDBChangeOpSetAttribute/Attribute:attcode+' => 'code of the modified property', ]); @@ -308,31 +307,31 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeScalar' => 'property change', - 'Class:CMDBChangeOpSetAttributeScalar+' => 'Object scalar properties change tracking', - 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue' => 'Previous value', + 'Class:CMDBChangeOpSetAttributeScalar' => 'property change', + 'Class:CMDBChangeOpSetAttributeScalar+' => 'Object scalar properties change tracking', + 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue' => 'Previous value', 'Class:CMDBChangeOpSetAttributeScalar/Attribute:oldvalue+' => 'previous value of the attribute', - 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue' => 'New value', + 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue' => 'New value', 'Class:CMDBChangeOpSetAttributeScalar/Attribute:newvalue+' => 'new value of the attribute', ]); // Used by CMDBChangeOp... & derived classes Dict::Add('EN US', 'English', 'English', [ - 'Change:ObjectCreated' => 'Object created', - 'Change:ObjectDeleted' => 'Object deleted', - 'Change:ObjectModified' => 'Object modified', - 'Change:TwoAttributesChanged' => 'Edited %1$s and %2$s', - 'Change:ThreeAttributesChanged' => 'Edited %1$s, %2$s and 1 other', - 'Change:FourOrMoreAttributesChanged' => 'Edited %1$s, %2$s and %3$s others', + 'Change:ObjectCreated' => 'Object created', + 'Change:ObjectDeleted' => 'Object deleted', + 'Change:ObjectModified' => 'Object modified', + 'Change:TwoAttributesChanged' => 'Edited %1$s and %2$s', + 'Change:ThreeAttributesChanged' => 'Edited %1$s, %2$s and 1 other', + 'Change:FourOrMoreAttributesChanged' => 'Edited %1$s, %2$s and %3$s others', 'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s set to %2$s (previous value: %3$s)', - 'Change:AttName_SetTo' => '%1$s set to %2$s', - 'Change:Text_AppendedTo_AttName' => '%1$s appended to %2$s', - 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', - 'Change:AttName_Changed' => '%1$s modified', - 'Change:AttName_EntryAdded' => '%1$s modified, new entry added: %2$s', - 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s', - 'Change:LinkSet:Added' => 'added %1$s', - 'Change:LinkSet:Removed' => 'removed %1$s', - 'Change:LinkSet:Modified' => 'modified %1$s', + 'Change:AttName_SetTo' => '%1$s set to %2$s', + 'Change:Text_AppendedTo_AttName' => '%1$s appended to %2$s', + 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s modified, previous value: %2$s', + 'Change:AttName_Changed' => '%1$s modified', + 'Change:AttName_EntryAdded' => '%1$s modified, new entry added: %2$s', + 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s', + 'Change:LinkSet:Added' => 'added %1$s', + 'Change:LinkSet:Removed' => 'removed %1$s', + 'Change:LinkSet:Modified' => 'modified %1$s', ]); // @@ -340,9 +339,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeBlob' => 'data change', - 'Class:CMDBChangeOpSetAttributeBlob+' => 'data change tracking', - 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata' => 'Previous data', + 'Class:CMDBChangeOpSetAttributeBlob' => 'data change', + 'Class:CMDBChangeOpSetAttributeBlob+' => 'data change tracking', + 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata' => 'Previous data', 'Class:CMDBChangeOpSetAttributeBlob/Attribute:prevdata+' => 'previous contents of the attribute', ]); @@ -351,9 +350,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:CMDBChangeOpSetAttributeText' => 'text change', - 'Class:CMDBChangeOpSetAttributeText+' => 'text change tracking', - 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata' => 'Previous data', + 'Class:CMDBChangeOpSetAttributeText' => 'text change', + 'Class:CMDBChangeOpSetAttributeText+' => 'text change tracking', + 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata' => 'Previous data', 'Class:CMDBChangeOpSetAttributeText/Attribute:prevdata+' => 'previous contents of the attribute', ]); @@ -362,15 +361,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Event' => 'Log Event', - 'Class:Event+' => 'An application internal event', - 'Class:Event/Attribute:message' => 'Message', - 'Class:Event/Attribute:message+' => 'short description of the event', - 'Class:Event/Attribute:date' => 'Date', - 'Class:Event/Attribute:date+' => 'date and time at which the changes have been recorded', - 'Class:Event/Attribute:userinfo' => 'User info', - 'Class:Event/Attribute:userinfo+' => 'identification of the user that was doing the action that triggered this event', - 'Class:Event/Attribute:finalclass' => 'Event sub-class', + 'Class:Event' => 'Log Event', + 'Class:Event+' => 'An application internal event', + 'Class:Event/Attribute:message' => 'Message', + 'Class:Event/Attribute:message+' => 'short description of the event', + 'Class:Event/Attribute:date' => 'Date', + 'Class:Event/Attribute:date+' => 'date and time at which the changes have been recorded', + 'Class:Event/Attribute:userinfo' => 'User info', + 'Class:Event/Attribute:userinfo+' => 'identification of the user that was doing the action that triggered this event', + 'Class:Event/Attribute:finalclass' => 'Event sub-class', 'Class:Event/Attribute:finalclass+' => 'Name of the final class: specifies the sort of event which occurred', ]); @@ -379,15 +378,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotification' => 'Notification sent', - 'Class:EventNotification+' => 'Trace of a notification that has been sent', - 'Class:EventNotification/Attribute:trigger_id' => 'Trigger', - 'Class:EventNotification/Attribute:trigger_id+' => '', - 'Class:EventNotification/Attribute:action_id' => 'Action', - 'Class:EventNotification/Attribute:action_id+' => '', - 'Class:EventNotification/Attribute:object_id' => 'Object id', - 'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)', - 'Class:EventNotification/Attribute:object_class' => 'Object class', + 'Class:EventNotification' => 'Notification sent', + 'Class:EventNotification+' => 'Trace of a notification that has been sent', + 'Class:EventNotification/Attribute:trigger_id' => 'Trigger', + 'Class:EventNotification/Attribute:trigger_id+' => '', + 'Class:EventNotification/Attribute:action_id' => 'Action', + 'Class:EventNotification/Attribute:action_id+' => '', + 'Class:EventNotification/Attribute:object_id' => 'Object id', + 'Class:EventNotification/Attribute:object_id+' => 'object id (class defined by the trigger ?)', + 'Class:EventNotification/Attribute:object_class' => 'Object class', 'Class:EventNotification/Attribute:object_class+' => 'Object class (Same as trigger)', ]); @@ -396,21 +395,21 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotificationEmail' => 'Email sent', - 'Class:EventNotificationEmail+' => 'Trace of an email that has been sent', - 'Class:EventNotificationEmail/Attribute:to' => 'TO', - 'Class:EventNotificationEmail/Attribute:to+' => '', - 'Class:EventNotificationEmail/Attribute:cc' => 'CC', - 'Class:EventNotificationEmail/Attribute:cc+' => '', - 'Class:EventNotificationEmail/Attribute:bcc' => 'BCC', - 'Class:EventNotificationEmail/Attribute:bcc+' => '', - 'Class:EventNotificationEmail/Attribute:from' => 'From', - 'Class:EventNotificationEmail/Attribute:from+' => 'Sender of the message', - 'Class:EventNotificationEmail/Attribute:subject' => 'Subject', - 'Class:EventNotificationEmail/Attribute:subject+' => '', - 'Class:EventNotificationEmail/Attribute:body' => 'Body', - 'Class:EventNotificationEmail/Attribute:body+' => '', - 'Class:EventNotificationEmail/Attribute:attachments' => 'Attachments', + 'Class:EventNotificationEmail' => 'Email sent', + 'Class:EventNotificationEmail+' => 'Trace of an email that has been sent', + 'Class:EventNotificationEmail/Attribute:to' => 'TO', + 'Class:EventNotificationEmail/Attribute:to+' => '', + 'Class:EventNotificationEmail/Attribute:cc' => 'CC', + 'Class:EventNotificationEmail/Attribute:cc+' => '', + 'Class:EventNotificationEmail/Attribute:bcc' => 'BCC', + 'Class:EventNotificationEmail/Attribute:bcc+' => '', + 'Class:EventNotificationEmail/Attribute:from' => 'From', + 'Class:EventNotificationEmail/Attribute:from+' => 'Sender of the message', + 'Class:EventNotificationEmail/Attribute:subject' => 'Subject', + 'Class:EventNotificationEmail/Attribute:subject+' => '', + 'Class:EventNotificationEmail/Attribute:body' => 'Body', + 'Class:EventNotificationEmail/Attribute:body+' => '', + 'Class:EventNotificationEmail/Attribute:attachments' => 'Attachments', 'Class:EventNotificationEmail/Attribute:attachments+' => '', ]); @@ -419,22 +418,22 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventIssue' => 'Issue event', - 'Class:EventIssue+' => 'Trace of an issue (warning, error, etc.)', - 'Class:EventIssue/Attribute:issue' => 'Issue', - 'Class:EventIssue/Attribute:issue+' => 'What happened', - 'Class:EventIssue/Attribute:impact' => 'Impact', - 'Class:EventIssue/Attribute:impact+' => 'What are the consequences', - 'Class:EventIssue/Attribute:page' => 'Page', - 'Class:EventIssue/Attribute:page+' => 'HTTP entry point', - 'Class:EventIssue/Attribute:arguments_post' => 'Posted arguments', + 'Class:EventIssue' => 'Issue event', + 'Class:EventIssue+' => 'Trace of an issue (warning, error, etc.)', + 'Class:EventIssue/Attribute:issue' => 'Issue', + 'Class:EventIssue/Attribute:issue+' => 'What happened', + 'Class:EventIssue/Attribute:impact' => 'Impact', + 'Class:EventIssue/Attribute:impact+' => 'What are the consequences', + 'Class:EventIssue/Attribute:page' => 'Page', + 'Class:EventIssue/Attribute:page+' => 'HTTP entry point', + 'Class:EventIssue/Attribute:arguments_post' => 'Posted arguments', 'Class:EventIssue/Attribute:arguments_post+' => 'HTTP POST arguments', - 'Class:EventIssue/Attribute:arguments_get' => 'URL arguments', - 'Class:EventIssue/Attribute:arguments_get+' => 'HTTP GET arguments', - 'Class:EventIssue/Attribute:callstack' => 'Callstack', - 'Class:EventIssue/Attribute:callstack+' => '', - 'Class:EventIssue/Attribute:data' => 'Data', - 'Class:EventIssue/Attribute:data+' => 'More information', + 'Class:EventIssue/Attribute:arguments_get' => 'URL arguments', + 'Class:EventIssue/Attribute:arguments_get+' => 'HTTP GET arguments', + 'Class:EventIssue/Attribute:callstack' => 'Callstack', + 'Class:EventIssue/Attribute:callstack+' => '', + 'Class:EventIssue/Attribute:data' => 'Data', + 'Class:EventIssue/Attribute:data+' => 'More information', ]); // @@ -442,37 +441,37 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventWebService' => 'Web service event', - 'Class:EventWebService+' => 'Trace of a web service call', - 'Class:EventWebService/Attribute:verb' => 'Verb', - 'Class:EventWebService/Attribute:verb+' => 'Name of the operation', - 'Class:EventWebService/Attribute:result' => 'Result', - 'Class:EventWebService/Attribute:result+' => 'Overall success/failure', - 'Class:EventWebService/Attribute:log_info' => 'Info log', - 'Class:EventWebService/Attribute:log_info+' => 'Result info log', - 'Class:EventWebService/Attribute:log_warning' => 'Warning log', + 'Class:EventWebService' => 'Web service event', + 'Class:EventWebService+' => 'Trace of a web service call', + 'Class:EventWebService/Attribute:verb' => 'Verb', + 'Class:EventWebService/Attribute:verb+' => 'Name of the operation', + 'Class:EventWebService/Attribute:result' => 'Result', + 'Class:EventWebService/Attribute:result+' => 'Overall success/failure', + 'Class:EventWebService/Attribute:log_info' => 'Info log', + 'Class:EventWebService/Attribute:log_info+' => 'Result info log', + 'Class:EventWebService/Attribute:log_warning' => 'Warning log', 'Class:EventWebService/Attribute:log_warning+' => 'Result warning log', - 'Class:EventWebService/Attribute:log_error' => 'Error log', - 'Class:EventWebService/Attribute:log_error+' => 'Result error log', - 'Class:EventWebService/Attribute:data' => 'Data', - 'Class:EventWebService/Attribute:data+' => 'Result data', + 'Class:EventWebService/Attribute:log_error' => 'Error log', + 'Class:EventWebService/Attribute:log_error+' => 'Result error log', + 'Class:EventWebService/Attribute:data' => 'Data', + 'Class:EventWebService/Attribute:data+' => 'Result data', ]); Dict::Add('EN US', 'English', 'English', [ - 'Class:EventRestService' => 'REST/JSON call', - 'Class:EventRestService+' => 'Trace of a REST/JSON service call', - 'Class:EventRestService/Attribute:operation' => 'Operation', - 'Class:EventRestService/Attribute:operation+' => 'Argument \'operation\'', - 'Class:EventRestService/Attribute:version' => 'Version', - 'Class:EventRestService/Attribute:version+' => 'Argument \'version\'', - 'Class:EventRestService/Attribute:json_input' => 'Input', - 'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'', - 'Class:EventRestService/Attribute:code' => 'Code', - 'Class:EventRestService/Attribute:code+' => 'Result code', - 'Class:EventRestService/Attribute:json_output' => 'Response', + 'Class:EventRestService' => 'REST/JSON call', + 'Class:EventRestService+' => 'Trace of a REST/JSON service call', + 'Class:EventRestService/Attribute:operation' => 'Operation', + 'Class:EventRestService/Attribute:operation+' => 'Argument \'operation\'', + 'Class:EventRestService/Attribute:version' => 'Version', + 'Class:EventRestService/Attribute:version+' => 'Argument \'version\'', + 'Class:EventRestService/Attribute:json_input' => 'Input', + 'Class:EventRestService/Attribute:json_input+' => 'Argument \'json_data\'', + 'Class:EventRestService/Attribute:code' => 'Code', + 'Class:EventRestService/Attribute:code+' => 'Result code', + 'Class:EventRestService/Attribute:json_output' => 'Response', 'Class:EventRestService/Attribute:json_output+' => 'HTTP response (json)', - 'Class:EventRestService/Attribute:provider' => 'Provider', - 'Class:EventRestService/Attribute:provider+' => 'PHP class implementing the expected operation', + 'Class:EventRestService/Attribute:provider' => 'Provider', + 'Class:EventRestService/Attribute:provider+' => 'PHP class implementing the expected operation', ]); // @@ -480,13 +479,13 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventLoginUsage' => 'Login Usage', - 'Class:EventLoginUsage+' => 'Connection to the application', - 'Class:EventLoginUsage/Attribute:user_id' => 'Login', - 'Class:EventLoginUsage/Attribute:user_id+' => '', - 'Class:EventLoginUsage/Attribute:contact_name' => 'User Name', - 'Class:EventLoginUsage/Attribute:contact_name+' => '', - 'Class:EventLoginUsage/Attribute:contact_email' => 'User Email', + 'Class:EventLoginUsage' => 'Login Usage', + 'Class:EventLoginUsage+' => 'Connection to the application', + 'Class:EventLoginUsage/Attribute:user_id' => 'Login', + 'Class:EventLoginUsage/Attribute:user_id+' => '', + 'Class:EventLoginUsage/Attribute:contact_name' => 'User Name', + 'Class:EventLoginUsage/Attribute:contact_name+' => '', + 'Class:EventLoginUsage/Attribute:contact_email' => 'User Email', 'Class:EventLoginUsage/Attribute:contact_email+' => 'Email Address of the User', ]); @@ -495,36 +494,36 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:EventNotificationNewsroom' => 'News sent', - 'Class:EventNotificationNewsroom+' => '', - 'Class:EventNotificationNewsroom/Attribute:title' => 'Title', - 'Class:EventNotificationNewsroom/Attribute:title+' => '', - 'Class:EventNotificationNewsroom/Attribute:icon' => 'Icon', - 'Class:EventNotificationNewsroom/Attribute:icon+' => '', - 'Class:EventNotificationNewsroom/Attribute:priority' => 'Priority', - 'Class:EventNotificationNewsroom/Attribute:priority+' => '', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:1' => 'Critical', + 'Class:EventNotificationNewsroom' => 'News sent', + 'Class:EventNotificationNewsroom+' => '', + 'Class:EventNotificationNewsroom/Attribute:title' => 'Title', + 'Class:EventNotificationNewsroom/Attribute:title+' => '', + 'Class:EventNotificationNewsroom/Attribute:icon' => 'Icon', + 'Class:EventNotificationNewsroom/Attribute:icon+' => '', + 'Class:EventNotificationNewsroom/Attribute:priority' => 'Priority', + 'Class:EventNotificationNewsroom/Attribute:priority+' => '', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:1' => 'Critical', 'Class:EventNotificationNewsroom/Attribute:priority/Value:1+' => 'Critical', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:2' => 'Urgent', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:2' => 'Urgent', 'Class:EventNotificationNewsroom/Attribute:priority/Value:2+' => 'Urgent', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:3' => 'Important', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:3' => 'Important', 'Class:EventNotificationNewsroom/Attribute:priority/Value:3+' => 'Important', - 'Class:EventNotificationNewsroom/Attribute:priority/Value:4' => 'Standard', + 'Class:EventNotificationNewsroom/Attribute:priority/Value:4' => 'Standard', 'Class:EventNotificationNewsroom/Attribute:priority/Value:4+' => 'Standard', - 'Class:EventNotificationNewsroom/Attribute:url' => 'URL', - 'Class:EventNotificationNewsroom/Attribute:url+' => '', - 'Class:EventNotificationNewsroom/Attribute:read' => 'Read', - 'Class:EventNotificationNewsroom/Attribute:read+' => '', - 'Class:EventNotificationNewsroom/Attribute:read/Value:no' => 'No', - 'Class:EventNotificationNewsroom/Attribute:read/Value:no+' => 'No', - 'Class:EventNotificationNewsroom/Attribute:read/Value:yes' => 'Yes', - 'Class:EventNotificationNewsroom/Attribute:read/Value:yes+' => 'Yes', - 'Class:EventNotificationNewsroom/Attribute:read_date' => 'Read date', - 'Class:EventNotificationNewsroom/Attribute:read_date+' => '', - 'Class:EventNotificationNewsroom/Attribute:contact_id' => 'Contact', - 'Class:EventNotificationNewsroom/Attribute:contact_id+' => '', - 'Core:EventNotificationNewsroom:ErrorNotificationNotSent' => 'Notification not sent', - 'Core:EventNotificationNewsroom:ErrorOnDBInsert' => 'An error occurred while saving the notification', + 'Class:EventNotificationNewsroom/Attribute:url' => 'URL', + 'Class:EventNotificationNewsroom/Attribute:url+' => '', + 'Class:EventNotificationNewsroom/Attribute:read' => 'Read', + 'Class:EventNotificationNewsroom/Attribute:read+' => '', + 'Class:EventNotificationNewsroom/Attribute:read/Value:no' => 'No', + 'Class:EventNotificationNewsroom/Attribute:read/Value:no+' => 'No', + 'Class:EventNotificationNewsroom/Attribute:read/Value:yes' => 'Yes', + 'Class:EventNotificationNewsroom/Attribute:read/Value:yes+' => 'Yes', + 'Class:EventNotificationNewsroom/Attribute:read_date' => 'Read date', + 'Class:EventNotificationNewsroom/Attribute:read_date+' => '', + 'Class:EventNotificationNewsroom/Attribute:contact_id' => 'Contact', + 'Class:EventNotificationNewsroom/Attribute:contact_id+' => '', + 'Core:EventNotificationNewsroom:ErrorNotificationNotSent' => 'Notification not sent', + 'Core:EventNotificationNewsroom:ErrorOnDBInsert' => 'An error occurred while saving the notification', ]); // @@ -532,35 +531,35 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Action' => 'Action', - 'Class:Action+' => 'User defined action', - 'Class:Action/ComplementaryName' => '%1$s: %2$s', - 'Class:Action/Attribute:name' => 'Name', - 'Class:Action/Attribute:name+' => 'Any value that is meaningful to distinguish this action from the others', - 'Class:Action/Attribute:description' => 'Description', - 'Class:Action/Attribute:description+' => 'A longer explanation about the purpose of this action. For information only.', - 'Class:Action/Attribute:status' => 'Status', - 'Class:Action/Attribute:status+' => 'This status drives the action behavior', - 'Class:Action/Attribute:status/Value:test' => 'Being tested', - 'Class:Action/Attribute:status/Value:test+' => '', - 'Class:Action/Attribute:status/Value:enabled' => 'In production', - 'Class:Action/Attribute:status/Value:enabled+' => '', - 'Class:Action/Attribute:status/Value:disabled' => 'Inactive', - 'Class:Action/Attribute:status/Value:disabled+' => '', - 'Class:Action/Attribute:trigger_list' => 'Related Triggers', - 'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action', - 'Class:Action/Attribute:asynchronous' => 'Asynchronous', - 'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in the background or not', + 'Class:Action' => 'Action', + 'Class:Action+' => 'User defined action', + 'Class:Action/ComplementaryName' => '%1$s: %2$s', + 'Class:Action/Attribute:name' => 'Name', + 'Class:Action/Attribute:name+' => 'Any value that is meaningful to distinguish this action from the others', + 'Class:Action/Attribute:description' => 'Description', + 'Class:Action/Attribute:description+' => 'A longer explanation about the purpose of this action. For information only.', + 'Class:Action/Attribute:status' => 'Status', + 'Class:Action/Attribute:status+' => 'This status drives the action behavior', + 'Class:Action/Attribute:status/Value:test' => 'Being tested', + 'Class:Action/Attribute:status/Value:test+' => '', + 'Class:Action/Attribute:status/Value:enabled' => 'In production', + 'Class:Action/Attribute:status/Value:enabled+' => '', + 'Class:Action/Attribute:status/Value:disabled' => 'Inactive', + 'Class:Action/Attribute:status/Value:disabled+' => '', + 'Class:Action/Attribute:trigger_list' => 'Related Triggers', + 'Class:Action/Attribute:trigger_list+' => 'Triggers linked to this action', + 'Class:Action/Attribute:asynchronous' => 'Asynchronous', + 'Class:Action/Attribute:asynchronous+' => 'Whether this action should be executed in the background or not', 'Class:Action/Attribute:asynchronous/Value:use_global_setting' => 'Use global setting', - 'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes', - 'Class:Action/Attribute:asynchronous/Value:no' => 'No', - 'Class:Action/Attribute:finalclass' => 'Action sub-class', - 'Class:Action/Attribute:finalclass+' => 'Name of the final class', - 'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.', - 'Action:last_executions_tab' => 'Last executions', - 'Action:last_executions_tab_panel_title' => 'Executions of this action (%1$s)', - 'Action:last_executions_tab_limit_days' => 'past %1$s days', - 'Action:last_executions_tab_limit_none' => 'no limit', + 'Class:Action/Attribute:asynchronous/Value:yes' => 'Yes', + 'Class:Action/Attribute:asynchronous/Value:no' => 'No', + 'Class:Action/Attribute:finalclass' => 'Action sub-class', + 'Class:Action/Attribute:finalclass+' => 'Name of the final class', + 'Action:WarningNoTriggerLinked' => 'Warning, no trigger is linked to the action. It will not be active until it has at least 1.', + 'Action:last_executions_tab' => 'Last executions', + 'Action:last_executions_tab_panel_title' => 'Executions of this action (%1$s)', + 'Action:last_executions_tab_limit_days' => 'past %1$s days', + 'Action:last_executions_tab_limit_none' => 'no limit', ]); // @@ -568,9 +567,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ActionNotification' => 'Notification Action', - 'Class:ActionNotification+' => 'Notification Action (abstract)', - 'Class:ActionNotification/Attribute:language' => 'Language', + 'Class:ActionNotification' => 'Notification Action', + 'Class:ActionNotification+' => 'Notification Action (abstract)', + 'Class:ActionNotification/Attribute:language' => 'Language', 'Class:ActionNotification/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)', ]); @@ -579,15 +578,15 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', - 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', - 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', + 'Class:lnkActionNotificationToContact' => 'Link ActionNotification / Contact', + 'Class:lnkActionNotificationToContact+' => 'Contact subscription to Notification Action', + 'Class:lnkActionNotificationToContact/Attribute:contact_id' => 'Contact', 'Class:lnkActionNotificationToContact/Attribute:contact_id+' => 'Contact who subscribed (or not) to the notification', - 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', - 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', - 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', + 'Class:lnkActionNotificationToContact/Attribute:action_id' => 'Action', + 'Class:lnkActionNotificationToContact/Attribute:action_id+' => 'The notification that the contact received at least once, and to which he can subscribe or unsubscribe', + 'Class:lnkActionNotificationToContact/Attribute:trigger_id' => 'Trigger', 'Class:lnkActionNotificationToContact/Attribute:trigger_id+' => 'The trigger that fired the notification', - 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', + 'Class:lnkActionNotificationToContact/Attribute:subscribed' => 'Subscribed', 'Class:lnkActionNotificationToContact/Attribute:subscribed+' => 'If the contact unsubscribed (no) or is subscribed (yes and default) to the notification', ]); @@ -671,40 +670,40 @@ // Dict::Add('EN US', 'English', 'English', [ - 'ActionNewsroom:trigger' => 'Trigger', - 'ActionNewsroom:content' => 'Message', - 'ActionNewsroom:settings' => 'Settings', - 'Class:ActionNewsroom' => 'Notification by Newsroom', - 'Class:ActionNewsroom+' => '', - 'Class:ActionNewsroom/Attribute:title' => 'Title', - 'Class:ActionNewsroom/Attribute:title+' => 'Title of the news. Can contain placeholders like $this->attribute_code$', - 'Class:ActionNewsroom/Attribute:message' => 'Message', - 'Class:ActionNewsroom/Attribute:message+' => 'Contents of the news, in Markdown format not HTML. Can contain placeholders like: + 'ActionNewsroom:trigger' => 'Trigger', + 'ActionNewsroom:content' => 'Message', + 'ActionNewsroom:settings' => 'Settings', + 'Class:ActionNewsroom' => 'Notification by Newsroom', + 'Class:ActionNewsroom+' => '', + 'Class:ActionNewsroom/Attribute:title' => 'Title', + 'Class:ActionNewsroom/Attribute:title+' => 'Title of the news. Can contain placeholders like $this->attribute_code$', + 'Class:ActionNewsroom/Attribute:message' => 'Message', + 'Class:ActionNewsroom/Attribute:message+' => 'Contents of the news, in Markdown format not HTML. Can contain placeholders like: - $this->attribute_code$ any attribute of the object triggering the notification, - $this->attribute_external_key->attribute$ recursive syntax for any remote attribute, - $current_contact->attribute$ attribute of the Person who triggered the notification', - 'Class:ActionNewsroom/Attribute:icon' => 'Icon', - 'Class:ActionNewsroom/Attribute:icon+' => 'Icon to appear next to the news in the newsroom. + 'Class:ActionNewsroom/Attribute:icon' => 'Icon', + 'Class:ActionNewsroom/Attribute:icon+' => 'Icon to appear next to the news in the newsroom. - If filled, the custom icon will be used - Else the icon of the triggering object if there is one (e.g. picture of a Person), - Else the icon of the triggering object class, - Otherwise, the application compact logo will be used', - 'Class:ActionNewsroom/Attribute:priority' => 'Priority', - 'Class:ActionNewsroom/Attribute:priority+' => 'News will be ordered by decreasing priority, when displayed in the Newsroom popup', - 'Class:ActionNewsroom/Attribute:priority/Value:1' => 'Critical', - 'Class:ActionNewsroom/Attribute:priority/Value:1+' => 'Critical', - 'Class:ActionNewsroom/Attribute:priority/Value:2' => 'Urgent', - 'Class:ActionNewsroom/Attribute:priority/Value:2+' => 'Urgent', - 'Class:ActionNewsroom/Attribute:priority/Value:3' => 'Important', - 'Class:ActionNewsroom/Attribute:priority/Value:3+' => 'Important', - 'Class:ActionNewsroom/Attribute:priority/Value:4' => 'Standard', - 'Class:ActionNewsroom/Attribute:priority/Value:4+' => 'Standard', - 'Class:ActionNewsroom/Attribute:test_recipient_id' => 'Test recipient', + 'Class:ActionNewsroom/Attribute:priority' => 'Priority', + 'Class:ActionNewsroom/Attribute:priority+' => 'News will be ordered by decreasing priority, when displayed in the Newsroom popup', + 'Class:ActionNewsroom/Attribute:priority/Value:1' => 'Critical', + 'Class:ActionNewsroom/Attribute:priority/Value:1+' => 'Critical', + 'Class:ActionNewsroom/Attribute:priority/Value:2' => 'Urgent', + 'Class:ActionNewsroom/Attribute:priority/Value:2+' => 'Urgent', + 'Class:ActionNewsroom/Attribute:priority/Value:3' => 'Important', + 'Class:ActionNewsroom/Attribute:priority/Value:3+' => 'Important', + 'Class:ActionNewsroom/Attribute:priority/Value:4' => 'Standard', + 'Class:ActionNewsroom/Attribute:priority/Value:4+' => 'Standard', + 'Class:ActionNewsroom/Attribute:test_recipient_id' => 'Test recipient', 'Class:ActionNewsroom/Attribute:test_recipient_id+' => 'Person used instead of Recipients when notification is being tested', - 'Class:ActionNewsroom/Attribute:recipients' => 'Recipients', - 'Class:ActionNewsroom/Attribute:recipients+' => 'An OQL query returning Contact objects', - 'Class:ActionNewsroom/Attribute:url' => 'URL', - 'Class:ActionNewsroom/Attribute:url+' => 'By default, it points to the object triggering the notification. But you can also specify a custom URL.', + 'Class:ActionNewsroom/Attribute:recipients' => 'Recipients', + 'Class:ActionNewsroom/Attribute:recipients+' => 'An OQL query returning Contact objects', + 'Class:ActionNewsroom/Attribute:url' => 'URL', + 'Class:ActionNewsroom/Attribute:url+' => 'By default, it points to the object triggering the notification. But you can also specify a custom URL.', ]); // @@ -712,24 +711,24 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:Trigger' => 'Trigger', - 'Class:Trigger+' => 'Custom event handler', - 'Class:Trigger/ComplementaryName' => '%1$s, %2$s', - 'Class:Trigger/Attribute:description' => 'Description', - 'Class:Trigger/Attribute:description+' => 'Be precise as your users will base their potential unsubscription on this information', - 'Class:Trigger/Attribute:action_list' => 'Triggered actions', - 'Class:Trigger/Attribute:action_list+' => 'Actions performed when the trigger is activated', - 'Class:Trigger/Attribute:finalclass' => 'Trigger sub-class', - 'Class:Trigger/Attribute:finalclass+' => 'Name of the final class', - 'Class:Trigger/Attribute:context' => 'Context', - 'Class:Trigger/Attribute:context+' => 'Context to allow the trigger to start', - 'Class:Trigger/Attribute:complement' => 'Additional information', - 'Class:Trigger/Attribute:complement+' => 'Computed automatically in english for triggers derived from TriggerOnObject', - 'Class:Trigger/Attribute:subscription_policy' => 'Subscription policy', - 'Class:Trigger/Attribute:subscription_policy+' => 'Allows users to unsubscribe from the trigger', - 'Class:Trigger/Attribute:subscription_policy/Value:allow_no_channel' => 'Allow complete unsubscription', + 'Class:Trigger' => 'Trigger', + 'Class:Trigger+' => 'Custom event handler', + 'Class:Trigger/ComplementaryName' => '%1$s, %2$s', + 'Class:Trigger/Attribute:description' => 'Description', + 'Class:Trigger/Attribute:description+' => 'Be precise as your users will base their potential unsubscription on this information', + 'Class:Trigger/Attribute:action_list' => 'Triggered actions', + 'Class:Trigger/Attribute:action_list+' => 'Actions performed when the trigger is activated', + 'Class:Trigger/Attribute:finalclass' => 'Trigger sub-class', + 'Class:Trigger/Attribute:finalclass+' => 'Name of the final class', + 'Class:Trigger/Attribute:context' => 'Context', + 'Class:Trigger/Attribute:context+' => 'Context to allow the trigger to start', + 'Class:Trigger/Attribute:complement' => 'Additional information', + 'Class:Trigger/Attribute:complement+' => 'Computed automatically in english for triggers derived from TriggerOnObject', + 'Class:Trigger/Attribute:subscription_policy' => 'Subscription policy', + 'Class:Trigger/Attribute:subscription_policy+' => 'Allows users to unsubscribe from the trigger', + 'Class:Trigger/Attribute:subscription_policy/Value:allow_no_channel' => 'Allow complete unsubscription', 'Class:Trigger/Attribute:subscription_policy/Value:force_at_least_one_channel' => 'Force at least one channel (News or Email)', - 'Class:Trigger/Attribute:subscription_policy/Value:force_all_channels' => 'Deny unsubscription', + 'Class:Trigger/Attribute:subscription_policy/Value:force_all_channels' => 'Deny unsubscription', ]); // @@ -752,7 +751,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnPortalUpdate' => 'Trigger (when updated from the portal)', + 'Class:TriggerOnPortalUpdate' => 'Trigger (when updated from the portal)', 'Class:TriggerOnPortalUpdate+' => 'Trigger on an end-user\'s update from the portal', ]); @@ -761,9 +760,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateChange' => 'Trigger (on state change)', - 'Class:TriggerOnStateChange+' => 'Trigger on object state change', - 'Class:TriggerOnStateChange/Attribute:state' => 'State', + 'Class:TriggerOnStateChange' => 'Trigger (on state change)', + 'Class:TriggerOnStateChange+' => 'Trigger on object state change', + 'Class:TriggerOnStateChange/Attribute:state' => 'State', 'Class:TriggerOnStateChange/Attribute:state+' => '', ]); @@ -772,7 +771,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateEnter' => 'Trigger (on entering a state)', + 'Class:TriggerOnStateEnter' => 'Trigger (on entering a state)', 'Class:TriggerOnStateEnter+' => 'Trigger on object state change - entering', ]); @@ -781,7 +780,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnStateLeave' => 'Trigger (on leaving a state)', + 'Class:TriggerOnStateLeave' => 'Trigger (on leaving a state)', 'Class:TriggerOnStateLeave+' => 'Trigger on object state change - leaving', ]); @@ -790,7 +789,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectCreate' => 'Trigger (on object creation)', + 'Class:TriggerOnObjectCreate' => 'Trigger (on object creation)', 'Class:TriggerOnObjectCreate+' => 'Trigger on object creation of [a child class of] the given class', ]); @@ -799,7 +798,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectDelete' => 'Trigger (on object deletion)', + 'Class:TriggerOnObjectDelete' => 'Trigger (on object deletion)', 'Class:TriggerOnObjectDelete+' => 'Trigger on object deletion of [a child class of] the given class', ]); @@ -808,10 +807,10 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)', - 'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class', - 'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions', - 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields', + 'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)', + 'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class', + 'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions', + 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields', 'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '', ]); @@ -820,9 +819,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnObjectMention' => 'Trigger (on object mention)', - 'Class:TriggerOnObjectMention+' => 'Trigger on mention (@xxx) of an object of [a child class of] the given class in a log attribute', - 'Class:TriggerOnObjectMention/Attribute:mentioned_filter' => 'Mentioned filter', + 'Class:TriggerOnObjectMention' => 'Trigger (on object mention)', + 'Class:TriggerOnObjectMention+' => 'Trigger on mention (@xxx) of an object of [a child class of] the given class in a log attribute', + 'Class:TriggerOnObjectMention/Attribute:mentioned_filter' => 'Mentioned filter', 'Class:TriggerOnObjectMention/Attribute:mentioned_filter+' => 'Limit the list of mentioned objects which will activate the trigger. If empty, any mentioned object (of any class) will activate it.', ]); @@ -831,9 +830,9 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)', - 'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class', - 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields', + 'Class:TriggerOnAttributeBlobDownload' => 'Trigger (on object\'s document download)', + 'Class:TriggerOnAttributeBlobDownload+' => 'Trigger on object\'s document field download of [a child class of] the given class', + 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes' => 'Target fields', 'Class:TriggerOnAttributeBlobDownload/Attribute:target_attcodes+' => '', ]); @@ -842,11 +841,11 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:TriggerOnThresholdReached' => 'Trigger (on threshold)', - 'Class:TriggerOnThresholdReached+' => 'Trigger on Stop-Watch threshold reached', - 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => 'Stop watch', + 'Class:TriggerOnThresholdReached' => 'Trigger (on threshold)', + 'Class:TriggerOnThresholdReached+' => 'Trigger on Stop-Watch threshold reached', + 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code' => 'Stop watch', 'Class:TriggerOnThresholdReached/Attribute:stop_watch_code+' => '', - 'Class:TriggerOnThresholdReached/Attribute:threshold_index' => 'Threshold', + 'Class:TriggerOnThresholdReached/Attribute:threshold_index' => 'Threshold', 'Class:TriggerOnThresholdReached/Attribute:threshold_index+' => '', ]); @@ -855,18 +854,18 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:lnkTriggerAction' => 'Action/Trigger', - 'Class:lnkTriggerAction+' => 'Link between a trigger and an action', - 'Class:lnkTriggerAction/Attribute:action_id' => 'Action', - 'Class:lnkTriggerAction/Attribute:action_id+' => 'The action to be executed', - 'Class:lnkTriggerAction/Attribute:action_name' => 'Action', - 'Class:lnkTriggerAction/Attribute:action_name+' => '', - 'Class:lnkTriggerAction/Attribute:trigger_id' => 'Trigger', - 'Class:lnkTriggerAction/Attribute:trigger_id+' => '', - 'Class:lnkTriggerAction/Attribute:trigger_name' => 'Trigger', + 'Class:lnkTriggerAction' => 'Action/Trigger', + 'Class:lnkTriggerAction+' => 'Link between a trigger and an action', + 'Class:lnkTriggerAction/Attribute:action_id' => 'Action', + 'Class:lnkTriggerAction/Attribute:action_id+' => 'The action to be executed', + 'Class:lnkTriggerAction/Attribute:action_name' => 'Action', + 'Class:lnkTriggerAction/Attribute:action_name+' => '', + 'Class:lnkTriggerAction/Attribute:trigger_id' => 'Trigger', + 'Class:lnkTriggerAction/Attribute:trigger_id+' => '', + 'Class:lnkTriggerAction/Attribute:trigger_name' => 'Trigger', 'Class:lnkTriggerAction/Attribute:trigger_name+' => '', - 'Class:lnkTriggerAction/Attribute:order' => 'Order', - 'Class:lnkTriggerAction/Attribute:order+' => 'Actions execution order', + 'Class:lnkTriggerAction/Attribute:order' => 'Order', + 'Class:lnkTriggerAction/Attribute:order+' => 'Actions execution order', ]); // @@ -1061,162 +1060,162 @@ 'Class:SynchroLog/Attribute:stats_nb_obj_created' => 'Nb objects created', 'Class:SynchroLog/Attribute:stats_nb_obj_created_errors' => 'Nb or errors while creating', 'Class:SynchroLog/Attribute:stats_nb_obj_updated' => 'Nb objects updated', - 'Class:SynchroLog/Attribute:stats_nb_obj_updated_errors' => 'Nb errors while updating', - 'Class:SynchroLog/Attribute:stats_nb_replica_reconciled_errors' => 'Nb of errors during reconciliation', - 'Class:SynchroLog/Attribute:stats_nb_replica_disappeared_no_action' => 'Nb replica disappeared', - 'Class:SynchroLog/Attribute:stats_nb_obj_new_updated' => 'Nb objects updated', - 'Class:SynchroLog/Attribute:stats_nb_obj_new_unchanged' => 'Nb objects unchanged', - 'Class:SynchroLog/Attribute:last_error' => 'Last error', - 'Class:SynchroLog/Attribute:traces' => 'Traces', - 'Class:SynchroReplica' => 'Synchro Replica', - 'Class:SynchroReplica/Attribute:sync_source_id' => 'Synchro Data Source', - 'Class:SynchroReplica/Attribute:dest_id' => 'Destination object (ID)', - 'Class:SynchroReplica/Attribute:dest_class' => 'Destination type', - 'Class:SynchroReplica/Attribute:status_last_seen' => 'Last seen', - 'Class:SynchroReplica/Attribute:status' => 'Status', - 'Class:SynchroReplica/Attribute:status/Value:modified' => 'Modified', - 'Class:SynchroReplica/Attribute:status/Value:new' => 'New', - 'Class:SynchroReplica/Attribute:status/Value:obsolete' => 'Obsolete', - 'Class:SynchroReplica/Attribute:status/Value:orphan' => 'Orphan', - 'Class:SynchroReplica/Attribute:status/Value:synchronized' => 'Synchronized', - 'Class:SynchroReplica/Attribute:status_dest_creator' => 'Object Created ?', - 'Class:SynchroReplica/Attribute:status_last_error' => 'Last Error', - 'Class:SynchroReplica/Attribute:status_last_warning' => 'Warnings', - 'Class:SynchroReplica/Attribute:info_creation_date' => 'Creation Date', - 'Class:SynchroReplica/Attribute:info_last_modified' => 'Last Modified Date', - 'Class:appUserPreferences' => 'User Preferences', - 'Class:appUserPreferences/Attribute:userid' => 'User', - 'Class:appUserPreferences/Attribute:preferences' => 'Prefs', - 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)', - 'Core:ExecProcess:Code255' => 'PHP Error (parsing, or runtime)', + 'Class:SynchroLog/Attribute:stats_nb_obj_updated_errors' => 'Nb errors while updating', + 'Class:SynchroLog/Attribute:stats_nb_replica_reconciled_errors' => 'Nb of errors during reconciliation', + 'Class:SynchroLog/Attribute:stats_nb_replica_disappeared_no_action' => 'Nb replica disappeared', + 'Class:SynchroLog/Attribute:stats_nb_obj_new_updated' => 'Nb objects updated', + 'Class:SynchroLog/Attribute:stats_nb_obj_new_unchanged' => 'Nb objects unchanged', + 'Class:SynchroLog/Attribute:last_error' => 'Last error', + 'Class:SynchroLog/Attribute:traces' => 'Traces', + 'Class:SynchroReplica' => 'Synchro Replica', + 'Class:SynchroReplica/Attribute:sync_source_id' => 'Synchro Data Source', + 'Class:SynchroReplica/Attribute:dest_id' => 'Destination object (ID)', + 'Class:SynchroReplica/Attribute:dest_class' => 'Destination type', + 'Class:SynchroReplica/Attribute:status_last_seen' => 'Last seen', + 'Class:SynchroReplica/Attribute:status' => 'Status', + 'Class:SynchroReplica/Attribute:status/Value:modified' => 'Modified', + 'Class:SynchroReplica/Attribute:status/Value:new' => 'New', + 'Class:SynchroReplica/Attribute:status/Value:obsolete' => 'Obsolete', + 'Class:SynchroReplica/Attribute:status/Value:orphan' => 'Orphan', + 'Class:SynchroReplica/Attribute:status/Value:synchronized' => 'Synchronized', + 'Class:SynchroReplica/Attribute:status_dest_creator' => 'Object Created ?', + 'Class:SynchroReplica/Attribute:status_last_error' => 'Last Error', + 'Class:SynchroReplica/Attribute:status_last_warning' => 'Warnings', + 'Class:SynchroReplica/Attribute:info_creation_date' => 'Creation Date', + 'Class:SynchroReplica/Attribute:info_last_modified' => 'Last Modified Date', + 'Class:appUserPreferences' => 'User Preferences', + 'Class:appUserPreferences/Attribute:userid' => 'User', + 'Class:appUserPreferences/Attribute:preferences' => 'Prefs', + 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)', + 'Core:ExecProcess:Code255' => 'PHP Error (parsing, or runtime)', // Attribute Duration - 'Core:Duration_Seconds' => '%1$ds', - 'Core:Duration_Minutes_Seconds' => '%1$dmin %2$ds', - 'Core:Duration_Hours_Minutes_Seconds' => '%1$dh %2$dmin %3$ds', - 'Core:Duration_Days_Hours_Minutes_Seconds' => '%1$sd %2$dh %3$dmin %4$ds', + 'Core:Duration_Seconds' => '%1$ds', + 'Core:Duration_Minutes_Seconds' => '%1$dmin %2$ds', + 'Core:Duration_Hours_Minutes_Seconds' => '%1$dh %2$dmin %3$ds', + 'Core:Duration_Days_Hours_Minutes_Seconds' => '%1$sd %2$dh %3$dmin %4$ds', // Explain working time computing - 'Core:ExplainWTC:ElapsedTime' => 'Time elapsed (stored as "%1$s")', - 'Core:ExplainWTC:StopWatch-TimeSpent' => 'Time spent for "%1$s"', - 'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%', + 'Core:ExplainWTC:ElapsedTime' => 'Time elapsed (stored as "%1$s")', + 'Core:ExplainWTC:StopWatch-TimeSpent' => 'Time spent for "%1$s"', + 'Core:ExplainWTC:StopWatch-Deadline' => 'Deadline for "%1$s" at %2$d%%', // Bulk export - 'Core:BulkExport:MissingParameter_Param' => 'Missing parameter "%1$s"', - 'Core:BulkExport:InvalidParameter_Query' => 'Invalid value for the parameter "query". There is no Query Phrasebook corresponding to the id: "%1$s".', - 'Core:BulkExport:ExportFormatPrompt' => 'Export format:', - 'Core:BulkExportOf_Class' => '%1$s Export', - 'Core:BulkExport:ClickHereToDownload_FileName' => 'Click here to download %1$s', - 'Core:BulkExport:ExportResult' => 'Result of the export:', - 'Core:BulkExport:RetrievingData' => 'Retrieving data...', - 'Core:BulkExport:HTMLFormat' => 'Web Page (*.html)', - 'Core:BulkExport:CSVFormat' => 'Comma Separated Values (*.csv)', - 'Core:BulkExport:XLSXFormat' => 'Excel 2007 or newer (*.xlsx)', - 'Core:BulkExport:PDFFormat' => 'PDF Document (*.pdf)', - 'Core:BulkExport:DragAndDropHelp' => 'Drag and drop the columns\' headers to arrange the columns. Preview of %1$s lines. Total number of lines to export: %2$s.', - 'Core:BulkExport:EmptyPreview' => 'Select the columns to be exported from the list above', - 'Core:BulkExport:ColumnsOrder' => 'Columns order', - 'Core:BulkExport:AvailableColumnsFrom_Class' => 'Available columns from %1$s', - 'Core:BulkExport:NoFieldSelected' => 'Select at least one column to be exported', - 'Core:BulkExport:CheckAll' => 'Check All', - 'Core:BulkExport:UncheckAll' => 'Uncheck All', - 'Core:BulkExport:ExportCancelledByUser' => 'Export cancelled by the user', - 'Core:BulkExport:CSVOptions' => 'CSV Options', - 'Core:BulkExport:CSVLocalization' => 'Localization', - 'Core:BulkExport:PDFOptions' => 'PDF Options', - 'Core:BulkExport:PDFPageFormat' => 'Page Format', - 'Core:BulkExport:PDFPageSize' => 'Page Size:', - 'Core:BulkExport:PageSize-A4' => 'A4', - 'Core:BulkExport:PageSize-A3' => 'A3', - 'Core:BulkExport:PageSize-Letter' => 'Letter', - 'Core:BulkExport:PDFPageOrientation' => 'Page Orientation:', - 'Core:BulkExport:PageOrientation-L' => 'Landscape', - 'Core:BulkExport:PageOrientation-P' => 'Portrait', - 'Core:BulkExport:XMLFormat' => 'XML file (*.xml)', - 'Core:BulkExport:XMLOptions' => 'XML Options', - 'Core:BulkExport:SpreadsheetFormat' => 'Spreadsheet HTML format (*.html)', - 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options', - 'Core:BulkExport:OptionNoLocalize' => 'Export Code instead of Label', - 'Core:BulkExport:OptionLinkSets' => 'Include linked objects', - 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting', - 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export', - 'Core:BulkExportLabelOQLExpression' => 'OQL Query:', - 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:', - 'Core:BulkExportMessageEmptyOQL' => 'Please enter a valid OQL query.', - 'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Please select a valid phrasebook entry.', - 'Core:BulkExportQueryPlaceholder' => 'Type an OQL query here...', - 'Core:BulkExportCanRunNonInteractive' => 'Click here to run the export in non-interactive mode.', - 'Core:BulkExportLegacyExport' => 'Click here to access the legacy export.', - 'Core:BulkExport:XLSXOptions' => 'Excel Options', - 'Core:BulkExport:TextFormat' => 'Text fields containing some HTML markup', - 'Core:BulkExport:DateTimeFormat' => 'Date and Time format', - 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Default format (%1$s), e.g. %2$s', - 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Custom format: %1$s', - 'Core:BulkExport:PDF:PageNumber' => 'Page %1$s', - 'Core:DateTime:Placeholder_d' => 'DD', // Day of the month: 2 digits (with leading zero) - 'Core:DateTime:Placeholder_j' => 'D', // Day of the month: 1 or 2 digits (without leading zero) - 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 - 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 - 'Core:DateTime:Placeholder_Y' => 'YYYY', // Year on 4 digits - 'Core:DateTime:Placeholder_y' => 'YY', // Year on 2 digits - 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 - 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 - 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 - 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 - 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) - 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) - 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 - 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 - 'Core:Validator:Default' => 'Wrong format', - 'Core:Validator:Mandatory' => 'Please, fill this field', - 'Core:Validator:MustBeInteger' => 'Must be an integer', - 'Core:Validator:MustSelectOne' => 'Please, select one', + 'Core:BulkExport:MissingParameter_Param' => 'Missing parameter "%1$s"', + 'Core:BulkExport:InvalidParameter_Query' => 'Invalid value for the parameter "query". There is no Query Phrasebook corresponding to the id: "%1$s".', + 'Core:BulkExport:ExportFormatPrompt' => 'Export format:', + 'Core:BulkExportOf_Class' => '%1$s Export', + 'Core:BulkExport:ClickHereToDownload_FileName' => 'Click here to download %1$s', + 'Core:BulkExport:ExportResult' => 'Result of the export:', + 'Core:BulkExport:RetrievingData' => 'Retrieving data...', + 'Core:BulkExport:HTMLFormat' => 'Web Page (*.html)', + 'Core:BulkExport:CSVFormat' => 'Comma Separated Values (*.csv)', + 'Core:BulkExport:XLSXFormat' => 'Excel 2007 or newer (*.xlsx)', + 'Core:BulkExport:PDFFormat' => 'PDF Document (*.pdf)', + 'Core:BulkExport:DragAndDropHelp' => 'Drag and drop the columns\' headers to arrange the columns. Preview of %1$s lines. Total number of lines to export: %2$s.', + 'Core:BulkExport:EmptyPreview' => 'Select the columns to be exported from the list above', + 'Core:BulkExport:ColumnsOrder' => 'Columns order', + 'Core:BulkExport:AvailableColumnsFrom_Class' => 'Available columns from %1$s', + 'Core:BulkExport:NoFieldSelected' => 'Select at least one column to be exported', + 'Core:BulkExport:CheckAll' => 'Check All', + 'Core:BulkExport:UncheckAll' => 'Uncheck All', + 'Core:BulkExport:ExportCancelledByUser' => 'Export cancelled by the user', + 'Core:BulkExport:CSVOptions' => 'CSV Options', + 'Core:BulkExport:CSVLocalization' => 'Localization', + 'Core:BulkExport:PDFOptions' => 'PDF Options', + 'Core:BulkExport:PDFPageFormat' => 'Page Format', + 'Core:BulkExport:PDFPageSize' => 'Page Size:', + 'Core:BulkExport:PageSize-A4' => 'A4', + 'Core:BulkExport:PageSize-A3' => 'A3', + 'Core:BulkExport:PageSize-Letter' => 'Letter', + 'Core:BulkExport:PDFPageOrientation' => 'Page Orientation:', + 'Core:BulkExport:PageOrientation-L' => 'Landscape', + 'Core:BulkExport:PageOrientation-P' => 'Portrait', + 'Core:BulkExport:XMLFormat' => 'XML file (*.xml)', + 'Core:BulkExport:XMLOptions' => 'XML Options', + 'Core:BulkExport:SpreadsheetFormat' => 'Spreadsheet HTML format (*.html)', + 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options', + 'Core:BulkExport:OptionNoLocalize' => 'Export Code instead of Label', + 'Core:BulkExport:OptionLinkSets' => 'Include linked objects', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting', + 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export', + 'Core:BulkExportLabelOQLExpression' => 'OQL Query:', + 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:', + 'Core:BulkExportMessageEmptyOQL' => 'Please enter a valid OQL query.', + 'Core:BulkExportMessageEmptyPhrasebookEntry' => 'Please select a valid phrasebook entry.', + 'Core:BulkExportQueryPlaceholder' => 'Type an OQL query here...', + 'Core:BulkExportCanRunNonInteractive' => 'Click here to run the export in non-interactive mode.', + 'Core:BulkExportLegacyExport' => 'Click here to access the legacy export.', + 'Core:BulkExport:XLSXOptions' => 'Excel Options', + 'Core:BulkExport:TextFormat' => 'Text fields containing some HTML markup', + 'Core:BulkExport:DateTimeFormat' => 'Date and Time format', + 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Default format (%1$s), e.g. %2$s', + 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Custom format: %1$s', + 'Core:BulkExport:PDF:PageNumber' => 'Page %1$s', + 'Core:DateTime:Placeholder_d' => 'DD', // Day of the month: 2 digits (with leading zero) + 'Core:DateTime:Placeholder_j' => 'D', // Day of the month: 1 or 2 digits (without leading zero) + 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 + 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 + 'Core:DateTime:Placeholder_Y' => 'YYYY', // Year on 4 digits + 'Core:DateTime:Placeholder_y' => 'YY', // Year on 2 digits + 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 + 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 + 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 + 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 + 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) + 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) + 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 + 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 + 'Core:Validator:Default' => 'Wrong format', + 'Core:Validator:Mandatory' => 'Please, fill this field', + 'Core:Validator:MustBeInteger' => 'Must be an integer', + 'Core:Validator:MustSelectOne' => 'Please, select one', ]); // // Class: TagSetFieldData // Dict::Add('EN US', 'English', 'English', [ - 'Class:TagSetFieldData' => '%2$s for class %1$s', + 'Class:TagSetFieldData' => '%2$s for class %1$s', 'Class:TagSetFieldData+' => '', - 'Class:TagSetFieldData/Attribute:code' => 'Code', - 'Class:TagSetFieldData/Attribute:code+' => 'Internal code. Must contain at least 3 alphanumeric characters', - 'Class:TagSetFieldData/Attribute:label' => 'Label', - 'Class:TagSetFieldData/Attribute:label+' => 'Displayed label', - 'Class:TagSetFieldData/Attribute:description' => 'Description', + 'Class:TagSetFieldData/Attribute:code' => 'Code', + 'Class:TagSetFieldData/Attribute:code+' => 'Internal code. Must contain at least 3 alphanumeric characters', + 'Class:TagSetFieldData/Attribute:label' => 'Label', + 'Class:TagSetFieldData/Attribute:label+' => 'Displayed label', + 'Class:TagSetFieldData/Attribute:description' => 'Description', 'Class:TagSetFieldData/Attribute:description+' => '', - 'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class', - 'Class:TagSetFieldData/Attribute:obj_class' => 'Object class', - 'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code', + 'Class:TagSetFieldData/Attribute:finalclass' => 'Tag class', + 'Class:TagSetFieldData/Attribute:obj_class' => 'Object class', + 'Class:TagSetFieldData/Attribute:obj_attcode' => 'Field code', - 'Core:TagSetFieldData:ErrorDeleteUsedTag' => 'Used tags cannot be deleted', + 'Core:TagSetFieldData:ErrorDeleteUsedTag' => 'Used tags cannot be deleted', 'Core:TagSetFieldData:ErrorDuplicateTagCodeOrLabel' => 'Tags codes or labels must be unique', - 'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.', - 'Core:TagSetFieldData:ErrorTagCodeReservedWord' => 'The chosen tag code is a reserved word', - 'Core:TagSetFieldData:ErrorTagLabelSyntax' => 'Tags label must not contain \'%1$s\' nor be empty', - 'Core:TagSetFieldData:ErrorCodeUpdateNotAllowed' => 'Tags Code cannot be changed when used', - 'Core:TagSetFieldData:ErrorClassUpdateNotAllowed' => 'Tags "Object Class" cannot be changed', + 'Core:TagSetFieldData:ErrorTagCodeSyntax' => 'Tags code must contain between 3 and %1$d alphanumeric characters, starting with a letter.', + 'Core:TagSetFieldData:ErrorTagCodeReservedWord' => 'The chosen tag code is a reserved word', + 'Core:TagSetFieldData:ErrorTagLabelSyntax' => 'Tags label must not contain \'%1$s\' nor be empty', + 'Core:TagSetFieldData:ErrorCodeUpdateNotAllowed' => 'Tags Code cannot be changed when used', + 'Core:TagSetFieldData:ErrorClassUpdateNotAllowed' => 'Tags "Object Class" cannot be changed', 'Core:TagSetFieldData:ErrorAttCodeUpdateNotAllowed' => 'Tags "Attribute Code" cannot be changed', - 'Core:TagSetFieldData:WhereIsThisTagTab' => 'Tag usage (%1$d)', - 'Core:TagSetFieldData:NoEntryFound' => 'No entry found for this tag', + 'Core:TagSetFieldData:WhereIsThisTagTab' => 'Tag usage (%1$d)', + 'Core:TagSetFieldData:NoEntryFound' => 'No entry found for this tag', ]); // // Class: DBProperty // Dict::Add('EN US', 'English', 'English', [ - 'Class:DBProperty' => 'DB property', - 'Class:DBProperty+' => '', - 'Class:DBProperty/Attribute:name' => 'Name', - 'Class:DBProperty/Attribute:name+' => '', - 'Class:DBProperty/Attribute:description' => 'Description', - 'Class:DBProperty/Attribute:description+' => '', - 'Class:DBProperty/Attribute:value' => 'Value', - 'Class:DBProperty/Attribute:value+' => '', - 'Class:DBProperty/Attribute:change_date' => 'Change date', - 'Class:DBProperty/Attribute:change_date+' => '', - 'Class:DBProperty/Attribute:change_comment' => 'Change comment', + 'Class:DBProperty' => 'DB property', + 'Class:DBProperty+' => '', + 'Class:DBProperty/Attribute:name' => 'Name', + 'Class:DBProperty/Attribute:name+' => '', + 'Class:DBProperty/Attribute:description' => 'Description', + 'Class:DBProperty/Attribute:description+' => '', + 'Class:DBProperty/Attribute:value' => 'Value', + 'Class:DBProperty/Attribute:value+' => '', + 'Class:DBProperty/Attribute:change_date' => 'Change date', + 'Class:DBProperty/Attribute:change_date+' => '', + 'Class:DBProperty/Attribute:change_comment' => 'Change comment', 'Class:DBProperty/Attribute:change_comment+' => '', ]); @@ -1224,59 +1223,59 @@ // Class: BackgroundTask // Dict::Add('EN US', 'English', 'English', [ - 'Class:BackgroundTask' => 'Background task', - 'Class:BackgroundTask+' => '', - 'Class:BackgroundTask/Attribute:class_name' => 'Class name', - 'Class:BackgroundTask/Attribute:class_name+' => '', - 'Class:BackgroundTask/Attribute:first_run_date' => 'First run date', - 'Class:BackgroundTask/Attribute:first_run_date+' => '', - 'Class:BackgroundTask/Attribute:latest_run_date' => 'Latest run date', - 'Class:BackgroundTask/Attribute:latest_run_date+' => '', - 'Class:BackgroundTask/Attribute:next_run_date' => 'Next run date', - 'Class:BackgroundTask/Attribute:next_run_date+' => '', - 'Class:BackgroundTask/Attribute:total_exec_count' => 'Total exec. count', - 'Class:BackgroundTask/Attribute:total_exec_count+' => '', - 'Class:BackgroundTask/Attribute:latest_run_duration' => 'Latest run duration', - 'Class:BackgroundTask/Attribute:latest_run_duration+' => '', - 'Class:BackgroundTask/Attribute:min_run_duration' => 'Min. run duration', - 'Class:BackgroundTask/Attribute:min_run_duration+' => '', - 'Class:BackgroundTask/Attribute:max_run_duration' => 'Max. run duration', - 'Class:BackgroundTask/Attribute:max_run_duration+' => '', - 'Class:BackgroundTask/Attribute:average_run_duration' => 'Average run duration', + 'Class:BackgroundTask' => 'Background task', + 'Class:BackgroundTask+' => '', + 'Class:BackgroundTask/Attribute:class_name' => 'Class name', + 'Class:BackgroundTask/Attribute:class_name+' => '', + 'Class:BackgroundTask/Attribute:first_run_date' => 'First run date', + 'Class:BackgroundTask/Attribute:first_run_date+' => '', + 'Class:BackgroundTask/Attribute:latest_run_date' => 'Latest run date', + 'Class:BackgroundTask/Attribute:latest_run_date+' => '', + 'Class:BackgroundTask/Attribute:next_run_date' => 'Next run date', + 'Class:BackgroundTask/Attribute:next_run_date+' => '', + 'Class:BackgroundTask/Attribute:total_exec_count' => 'Total exec. count', + 'Class:BackgroundTask/Attribute:total_exec_count+' => '', + 'Class:BackgroundTask/Attribute:latest_run_duration' => 'Latest run duration', + 'Class:BackgroundTask/Attribute:latest_run_duration+' => '', + 'Class:BackgroundTask/Attribute:min_run_duration' => 'Min. run duration', + 'Class:BackgroundTask/Attribute:min_run_duration+' => '', + 'Class:BackgroundTask/Attribute:max_run_duration' => 'Max. run duration', + 'Class:BackgroundTask/Attribute:max_run_duration+' => '', + 'Class:BackgroundTask/Attribute:average_run_duration' => 'Average run duration', 'Class:BackgroundTask/Attribute:average_run_duration+' => '', - 'Class:BackgroundTask/Attribute:running' => 'Running', - 'Class:BackgroundTask/Attribute:running+' => '', - 'Class:BackgroundTask/Attribute:status' => 'Status', - 'Class:BackgroundTask/Attribute:status+' => '', + 'Class:BackgroundTask/Attribute:running' => 'Running', + 'Class:BackgroundTask/Attribute:running+' => '', + 'Class:BackgroundTask/Attribute:status' => 'Status', + 'Class:BackgroundTask/Attribute:status+' => '', ]); // // Class: AsyncTask // Dict::Add('EN US', 'English', 'English', [ - 'Class:AsyncTask' => 'Async. task', - 'Class:AsyncTask+' => '', - 'Class:AsyncTask/Attribute:created' => 'Created', - 'Class:AsyncTask/Attribute:created+' => '', - 'Class:AsyncTask/Attribute:started' => 'Started', - 'Class:AsyncTask/Attribute:started+' => '', - 'Class:AsyncTask/Attribute:planned' => 'Planned', - 'Class:AsyncTask/Attribute:planned+' => '', - 'Class:AsyncTask/Attribute:event_id' => 'Event', - 'Class:AsyncTask/Attribute:event_id+' => '', - 'Class:AsyncTask/Attribute:finalclass' => 'Final class', - 'Class:AsyncTask/Attribute:finalclass+' => '', - 'Class:AsyncTask/Attribute:status' => 'Status', - 'Class:AsyncTask/Attribute:status+' => '', - 'Class:AsyncTask/Attribute:remaining_retries' => 'Remaining retries', - 'Class:AsyncTask/Attribute:remaining_retries+' => '', - 'Class:AsyncTask/Attribute:last_error_code' => 'Last error code', - 'Class:AsyncTask/Attribute:last_error_code+' => '', - 'Class:AsyncTask/Attribute:last_error' => 'Last error', - 'Class:AsyncTask/Attribute:last_error+' => '', - 'Class:AsyncTask/Attribute:last_attempt' => 'Last attempt', - 'Class:AsyncTask/Attribute:last_attempt+' => '', - 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]". Expecting an array with the following keys: %2$s', + 'Class:AsyncTask' => 'Async. task', + 'Class:AsyncTask+' => '', + 'Class:AsyncTask/Attribute:created' => 'Created', + 'Class:AsyncTask/Attribute:created+' => '', + 'Class:AsyncTask/Attribute:started' => 'Started', + 'Class:AsyncTask/Attribute:started+' => '', + 'Class:AsyncTask/Attribute:planned' => 'Planned', + 'Class:AsyncTask/Attribute:planned+' => '', + 'Class:AsyncTask/Attribute:event_id' => 'Event', + 'Class:AsyncTask/Attribute:event_id+' => '', + 'Class:AsyncTask/Attribute:finalclass' => 'Final class', + 'Class:AsyncTask/Attribute:finalclass+' => '', + 'Class:AsyncTask/Attribute:status' => 'Status', + 'Class:AsyncTask/Attribute:status+' => '', + 'Class:AsyncTask/Attribute:remaining_retries' => 'Remaining retries', + 'Class:AsyncTask/Attribute:remaining_retries+' => '', + 'Class:AsyncTask/Attribute:last_error_code' => 'Last error code', + 'Class:AsyncTask/Attribute:last_error_code+' => '', + 'Class:AsyncTask/Attribute:last_error' => 'Last error', + 'Class:AsyncTask/Attribute:last_error+' => '', + 'Class:AsyncTask/Attribute:last_attempt' => 'Last attempt', + 'Class:AsyncTask/Attribute:last_attempt+' => '', + 'Class:AsyncTask:InvalidConfig_Class_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]". Expecting an array with the following keys: %2$s', 'Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys' => 'Invalid format for the configuration of "async_task_retries[%1$s]": unexpected key "%2$s". Expecting only the following keys: %3$s', ]); @@ -1285,7 +1284,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:AbstractResource' => 'Abstract Resource', + 'Class:AbstractResource' => 'Abstract Resource', 'Class:AbstractResource+' => '', ]); @@ -1294,7 +1293,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceAdminMenu' => 'Resource Admin Menu', + 'Class:ResourceAdminMenu' => 'Resource Admin Menu', 'Class:ResourceAdminMenu+' => '', ]); @@ -1303,7 +1302,7 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceRunQueriesMenu' => 'Resource Run Queries Menu', + 'Class:ResourceRunQueriesMenu' => 'Resource Run Queries Menu', 'Class:ResourceRunQueriesMenu+' => '', ]); @@ -1312,6 +1311,6 @@ // Dict::Add('EN US', 'English', 'English', [ - 'Class:ResourceSystemMenu' => 'Resource System Menu', + 'Class:ResourceSystemMenu' => 'Resource System Menu', 'Class:ResourceSystemMenu+' => '', ]); From b61a83668533641cdb75f795ef2f3deadfd4515b Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 15:34:49 +0200 Subject: [PATCH 15/22] Check if class UserToken exists --- sources/Users/ITopUserCountingRepository.php | 5 ++++- .../Users/ITopUserCountingRepositoryTest.php | 17 ++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index e2356b9047..ccf6fece17 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -159,8 +159,11 @@ public function GetDisabledUsers(bool $bAllData = true): array /** * @throws Exception */ - public function GetApplicationUsers(bool $bAllData = true): array + public function GetApplicationUsers(bool $bAllData = true): ?array { + if (!MetaModel::IsValidClass('UserToken')) { + return null; + } $sOQLApplicationUser = 'SELECT UserToken'; try { $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 88417e52b7..117296c5b1 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -4,6 +4,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use Combodo\iTop\Users\ITopUserCountingRepository; +use MetaModel; use User; class ITopUserCountingRepositoryTest extends ItopDataTestCase @@ -14,7 +15,9 @@ protected function setUp(): void $this->CreateReadOnlyUsers(); $this->CreateDisabledUsers(); $this->CreatePortalUsers(); - $this->CreateTokenUsers(); + if (MetaModel::IsValidClass('UserToken')) { + $this->CreateTokenUsers(); + } $this->CreateConsoleUsers(); $this->CreateBusinessPartnerUser(); @@ -220,23 +223,23 @@ public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) public function RealUseCasesDataProvider() { return [ - [ + 'User with profiles Support Agent and Configuration ReadOnly' => [ 'profiles' => ['Support Agent', 'Configuration ReadOnly'], 'expected category' => 'console', ], - [ + 'User with profiles Configuration ReadOnly and Service Catalog ReadOnly' => [ 'profiles' => ['Configuration ReadOnly', 'Service Catalog ReadOnly'], 'expected category' => 'readonly', ], - [[ - 'profiles' => 'Portal user', 'Service Catalog ReadOnly'], + 'User with profiles Portal user and Service Catalog ReadOnly' => [ + 'profiles' => ['Portal user', 'Service Catalog ReadOnly'], 'expected category' => 'portal', ], - [ + 'User with profiles Support Agent and Portal user' => [ 'profiles' => ['Support Agent', 'Portal user'], 'expected category' => 'portal', ], - [ + 'User with profiles Configuration Manager and Ticket ReadOnly' => [ 'profiles' => ['Configuration Manager', 'Ticket ReadOnly'], 'expected category' => 'console', ], From 5fdd1dad858a0806b8d1f5300899d733bc5dfb72 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 15:50:23 +0200 Subject: [PATCH 16/22] Check if class UserToken exists --- sources/Users/ITopUserCountingRepository.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index ccf6fece17..0a36e470ed 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -133,8 +133,11 @@ public function GetReadOnlyUsers(): array } } - $aUserToken = $this->GetApplicationUsers(); - return array_diff($aReadOnlyUsers, $aUserToken); + if (MetaModel::IsValidClass('UserToken')) { + $aUserToken = $this->GetApplicationUsers(); + $aReadOnlyUsers = array_diff($aReadOnlyUsers, $aUserToken); + } + return $aReadOnlyUsers; } /** From 950617733b7abdd5190d4ed278ad5f1633c058b5 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 16:08:48 +0200 Subject: [PATCH 17/22] Check if class UserToken exists --- dictionaries/en.dictionary.itop.core.php | 15 ++++++++------- .../Users/ITopUserCountingRepositoryTest.php | 12 +++++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index 178c752bd9..01980b6477 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -217,13 +217,14 @@ 'Core:Context=CRON' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', - 'Core:GetCountingUsers:Error' => 'Error while getting %1$s quota', - 'Core:ConsoleUsers' => 'console users', - 'Core:DisabledUsers' => 'disabled users', - 'Core:PortalUsers' => 'portal users', - 'Core:BusinessPartnerUser' => 'business partner users', - 'Core:ReadOnlyUsers' => 'read-only users', - 'Core:ApplicationUsers' => 'application users', + 'Core:GetCountingUsers:Error' => 'Error while counting %1$s', + 'Core:CountingUsers:ConsoleUsers' => 'console users', + 'Core:CountingUsers:DisabledUsers' => 'disabled users', + 'Core:CountingUsers:PortalUsers' => 'portal users', + 'Core:CountingUsers:BusinessPartnerUser' => 'business partner users', + 'Core:CountingUsers:ReadOnlyUsers' => 'read - only users', + 'Core:CountingUsers:ApplicationUsers' => 'application users', + 'Core:CountingUsers:AllUsers' => 'all users', ]); ////////////////////////////////////////////////////////////////////// diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 117296c5b1..4c0c011386 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -94,9 +94,11 @@ public function testNotDuplicateInDifferentCountsCategories(): void 'portal' => $oITopUserRepository->GetPortalUsers(), 'disabled' => $oITopUserRepository->GetDisabledUsers(), 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), - 'application' => $oITopUserRepository->GetApplicationUsers(), 'businesspartner' => $oITopUserRepository->GetBusinessPartnerUsers(), ]; + if (MetaModel::IsValidClass('UserToken')) { + $aCountedUsers['application'] = $oITopUserRepository->GetApplicationUsers(); + } $aCountedUserFormated = []; foreach ($aCountedUsers as $sCountedCategory => $aUsers) { @@ -129,7 +131,9 @@ public function testAllUsersAreCounted() $aPortalUsers = $oITopUserRepository->GetPortalUsers(); $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); + if (MetaModel::IsValidClass('UserToken')) { + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; + } $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); @@ -174,7 +178,9 @@ public function testAllCountedUsersAreUsersObjects() $aPortalUsers = $oITopUserRepository->GetPortalUsers(); $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers(); + if (MetaModel::IsValidClass('UserToken')) { + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; + } $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); From 81832202d50177550fa653b0d01afc96a57d18fa Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 16:22:00 +0200 Subject: [PATCH 18/22] Remove assertion in test --- .../sources/Users/ITopUserCountingRepositoryTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index 4c0c011386..b3bf7df8cd 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -131,9 +131,7 @@ public function testAllUsersAreCounted() $aPortalUsers = $oITopUserRepository->GetPortalUsers(); $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - if (MetaModel::IsValidClass('UserToken')) { - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; - } + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); @@ -178,9 +176,7 @@ public function testAllCountedUsersAreUsersObjects() $aPortalUsers = $oITopUserRepository->GetPortalUsers(); $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - if (MetaModel::IsValidClass('UserToken')) { - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; - } + $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); From 422ca60964d55c827eeca6e3bc038cc8e18f6743 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Wed, 10 Jun 2026 17:25:53 +0200 Subject: [PATCH 19/22] Align dict key with the others --- dictionaries/en.dictionary.itop.core.php | 2 +- dictionaries/fr.dictionary.itop.core.php | 2 +- sources/Users/ITopUserCountingRepository.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index 01980b6477..da2aeb4fd2 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -221,7 +221,7 @@ 'Core:CountingUsers:ConsoleUsers' => 'console users', 'Core:CountingUsers:DisabledUsers' => 'disabled users', 'Core:CountingUsers:PortalUsers' => 'portal users', - 'Core:CountingUsers:BusinessPartnerUser' => 'business partner users', + 'Core:CountingUsers:BusinessPartnerUsers' => 'business partner users', 'Core:CountingUsers:ReadOnlyUsers' => 'read - only users', 'Core:CountingUsers:ApplicationUsers' => 'application users', 'Core:CountingUsers:AllUsers' => 'all users', diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 1541db4cc5..a249331655 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -166,7 +166,7 @@ 'Core:CountingUsers:ConsoleUsers' => 'utilisateurs console', 'Core:CountingUsers:DisabledUsers' => 'utilisateurs désactivés', 'Core:CountingUsers:PortalUsers' => 'utilisateurs du portail', - 'Core:CountingUsers:BusinessPartnerUser' => 'utilisateurs partenaires business', + 'Core:CountingUsers:BusinessPartnerUsers' => 'utilisateurs partenaires business', 'Core:CountingUsers:ReadOnlyUsers' => 'utilisateurs en lecture seule', 'Core:CountingUsers:ApplicationUsers' => 'utilisateurs applicatifs', 'Core:CountingUsers:AllUsers' => 'tous les utilisateurs', diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index 0a36e470ed..be9c86d6f5 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -105,8 +105,8 @@ public function GetBusinessPartnerUsers(bool $bAllData = true, array $aExcludedF try { $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUser')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUser')).'.'); + IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUsers')).' - error details : '.$e->getMessage()); + throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUsers')).'.'); } return $this->GetUsersFromFilter($oFilter); From 644ac1cb9b4c1d5401754412ed49a33cc8266150 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Thu, 11 Jun 2026 10:47:52 +0200 Subject: [PATCH 20/22] Refactor --- sources/Users/ITopUserCountingRepository.php | 20 +++++++------------ .../Users/ITopUserCountingRepositoryTest.php | 1 + 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php index be9c86d6f5..2f5fd29c2d 100644 --- a/sources/Users/ITopUserCountingRepository.php +++ b/sources/Users/ITopUserCountingRepository.php @@ -117,10 +117,10 @@ public function GetBusinessPartnerUsers(bool $bAllData = true, array $aExcludedF * @throws CoreException * @throws Exception */ - public function GetReadOnlyUsers(): array + public function GetReadOnlyUsers(bool $bAllData = true): array { $aReadOnlyUsers = []; - $aAllUsers = $this->GetAllUsers(); + $aAllUsers = $this->GetAllUsers($bAllData); /** @var User $oUser */ foreach ($aAllUsers as $oUser) { $bIsReadOnlyUser = true; @@ -133,11 +133,9 @@ public function GetReadOnlyUsers(): array } } - if (MetaModel::IsValidClass('UserToken')) { - $aUserToken = $this->GetApplicationUsers(); - $aReadOnlyUsers = array_diff($aReadOnlyUsers, $aUserToken); - } - return $aReadOnlyUsers; + $aUserToken = $this->GetApplicationUsers() ?? []; + + return array_diff($aReadOnlyUsers, $aUserToken); } /** @@ -205,22 +203,18 @@ public function GetAllUsers(bool $bAllData = true): array */ private function IsUserReadOnly(User $oUser, string $sClassCategory): bool { - // login (mandatory to compute rights) - UserRights::Login($oUser->GetName()); - foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { // no need to check stimuli for now since users can't execute any without UR_ACTION_MODIFY if ( UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) + UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) || + UserRights::IsActionAllowed($sClass, UR_ACTION_CREATE, null, $oUser) ) { - UserRights::Logoff(); return false; } } - UserRights::Logoff(); return true; } diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php index b3bf7df8cd..804d570a66 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php @@ -202,6 +202,7 @@ public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) { $iUser = $this->GivenUserInDB('<_ç"éue"ç_u', $aProfilesList, bReturnLogin: false); $oUser = \MetaModel::GetObject('User', $iUser); + \UserRights::Login($oUser->Get('login')); // work around since user needs to be logged in in IsActionAllowed $oITopUserRepository = new ITopUserCountingRepository(); $aUsers = match ($sExpectedCategorie) { 'console' => $oITopUserRepository->GetConsoleUsers(), From 20dc01ef154dcd7fdea90e802880d3c274fbba66 Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Thu, 11 Jun 2026 16:25:09 +0200 Subject: [PATCH 21/22] Removing user counting from iTop (to put it in itop-system-information) --- dictionaries/en.dictionary.itop.core.php | 9 - dictionaries/fr.dictionary.itop.core.php | 9 - lib/composer/autoload_classmap.php | 1 - lib/composer/autoload_static.php | 1 - sources/Users/ITopUserCountingRepository.php | 254 ------------------- 5 files changed, 274 deletions(-) delete mode 100644 sources/Users/ITopUserCountingRepository.php diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php index da2aeb4fd2..8b26efc141 100644 --- a/dictionaries/en.dictionary.itop.core.php +++ b/dictionaries/en.dictionary.itop.core.php @@ -216,15 +216,6 @@ 'Core:Context=GUI:Console' => 'Console', 'Core:Context=CRON' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', - - 'Core:GetCountingUsers:Error' => 'Error while counting %1$s', - 'Core:CountingUsers:ConsoleUsers' => 'console users', - 'Core:CountingUsers:DisabledUsers' => 'disabled users', - 'Core:CountingUsers:PortalUsers' => 'portal users', - 'Core:CountingUsers:BusinessPartnerUsers' => 'business partner users', - 'Core:CountingUsers:ReadOnlyUsers' => 'read - only users', - 'Core:CountingUsers:ApplicationUsers' => 'application users', - 'Core:CountingUsers:AllUsers' => 'all users', ]); ////////////////////////////////////////////////////////////////////// diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index a249331655..25e31bdb7a 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -161,15 +161,6 @@ 'Core:Context=CRON+' => 'cron', 'Core:Context=GUI:Portal' => 'Portal', 'Core:Context=GUI:Portal+' => 'GUI:Portal', - - 'Core:GetCountingUsers:Error' => 'Erreur lors du comptage des %1$s', - 'Core:CountingUsers:ConsoleUsers' => 'utilisateurs console', - 'Core:CountingUsers:DisabledUsers' => 'utilisateurs désactivés', - 'Core:CountingUsers:PortalUsers' => 'utilisateurs du portail', - 'Core:CountingUsers:BusinessPartnerUsers' => 'utilisateurs partenaires business', - 'Core:CountingUsers:ReadOnlyUsers' => 'utilisateurs en lecture seule', - 'Core:CountingUsers:ApplicationUsers' => 'utilisateurs applicatifs', - 'Core:CountingUsers:AllUsers' => 'tous les utilisateurs', ]); ////////////////////////////////////////////////////////////////////// diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 9bc9e59445..626b2db633 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -649,7 +649,6 @@ 'Combodo\\iTop\\SessionTracker\\SessionGC' => $baseDir . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => $baseDir . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => $baseDir . '/sources/SessionTracker/iSessionHandlerExtension.php', - 'Combodo\\iTop\\Users\\ITopUserCountingRepository' => $baseDir . '/sources/Users/ITopUserCountingRepository.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index edda3e727c..fe840689d7 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -1050,7 +1050,6 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\SessionTracker\\SessionGC' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionGC.php', 'Combodo\\iTop\\SessionTracker\\SessionHandler' => __DIR__ . '/../..' . '/sources/SessionTracker/SessionHandler.php', 'Combodo\\iTop\\SessionTracker\\iSessionHandlerExtension' => __DIR__ . '/../..' . '/sources/SessionTracker/iSessionHandlerExtension.php', - 'Combodo\\iTop\\Users\\ITopUserCountingRepository' => __DIR__ . '/../..' . '/sources/Users/ITopUserCountingRepository.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', diff --git a/sources/Users/ITopUserCountingRepository.php b/sources/Users/ITopUserCountingRepository.php deleted file mode 100644 index 2f5fd29c2d..0000000000 --- a/sources/Users/ITopUserCountingRepository.php +++ /dev/null @@ -1,254 +0,0 @@ -ArrayToOQLStringParameter($aExcludedUsers); - $sExcludedProfiles = $this->ArrayToOQLStringParameter($aExcludedProfiles); - $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); - - $sOQLUserConsole = " - SELECT User AS u - WHERE u.status != 'disabled' - AND u.login NOT IN ($sExcludedUsers) - AND u.finalclass NOT IN ($sExcludedFinalClasses) - AND id NOT IN ( - SELECT User AS uex - JOIN URP_UserProfile AS uup ON uup.userid = uex.id - JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.name IN ($sExcludedProfiles)) - "; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLUserConsole) : DBObjectSearch::FromOQL($sOQLUserConsole); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ConsoleUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ConsoleUsers')).'.'); - } - - $aConsoleUsers = $this->GetUsersFromFilter($oFilter); - $aReadOnlyUsers = $this->GetReadOnlyUsers(); - return array_diff($aConsoleUsers, $aReadOnlyUsers); - } - - /** - * @throws Exception - */ - public function GetPortalUsers(bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array - { - $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); - - $sOQLPortalUser = " - SELECT User AS u - JOIN URP_UserProfile AS uup ON uup.userid = u.id - JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.id = '2' AND u.status != 'disabled' AND u.finalclass NOT IN ($sExcludedFinalClasses)"; - - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:PortalUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:PortalUsers')).'.'); - } - - return $this->GetUsersFromFilter($oFilter); - } - - /** - * @throws Exception - */ - public function GetBusinessPartnerUsers(bool $bAllData = true, array $aExcludedFinalClasses = ['UserToken', 'UserRemoteSaaS']): array - { - $sExcludedFinalClasses = $this->ArrayToOQLStringParameter($aExcludedFinalClasses); - - $sOQLPortalUser = " - SELECT User AS u - JOIN URP_UserProfile AS uup ON uup.userid = u.id - JOIN URP_Profiles AS up ON uup.profileid = up.id - WHERE up.id = '40' AND u.status != 'disabled' AND u.finalclass NOT IN ($sExcludedFinalClasses)"; - - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLPortalUser) : DBObjectSearch::FromOQL($sOQLPortalUser); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:BusinessPartnerUsers')).'.'); - } - - return $this->GetUsersFromFilter($oFilter); - } - - /** - * @throws DictExceptionMissingString - * @throws CoreException - * @throws Exception - */ - public function GetReadOnlyUsers(bool $bAllData = true): array - { - $aReadOnlyUsers = []; - $aAllUsers = $this->GetAllUsers($bAllData); - /** @var User $oUser */ - foreach ($aAllUsers as $oUser) { - $bIsReadOnlyUser = true; - if (!$this->IsUserReadOnly($oUser, 'bizmodel') || - !$this->IsUserReadOnly($oUser, 'grant_by_profile')) { - $bIsReadOnlyUser = false; - } - if ($bIsReadOnlyUser) { - $aReadOnlyUsers[] = $oUser; - } - } - - $aUserToken = $this->GetApplicationUsers() ?? []; - - return array_diff($aReadOnlyUsers, $aUserToken); - } - - /** - * @throws Exception - */ - public function GetDisabledUsers(bool $bAllData = true): array - { - $sOQLDisabledUser = " - SELECT User AS u - WHERE u.status = 'disabled' - "; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLDisabledUser) : DBObjectSearch::FromOQL($sOQLDisabledUser); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:DisabledUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:DisabledUsers')).'.'); - } - - return $this->GetUsersFromFilter($oFilter); - } - - /** - * @throws Exception - */ - public function GetApplicationUsers(bool $bAllData = true): ?array - { - if (!MetaModel::IsValidClass('UserToken')) { - return null; - } - $sOQLApplicationUser = 'SELECT UserToken'; - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOQLApplicationUser) : DBObjectSearch::FromOQL($sOQLApplicationUser); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ApplicationUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:ApplicationUsers')).'.'); - } - - return $this->GetUsersFromFilter($oFilter); - } - - /** - * @throws Exception - */ - public function GetAllUsers(bool $bAllData = true): array - { - $sOqlUser = 'SELECT User'; - - try { - $oFilter = $bAllData ? DBObjectSearch::FromOQL_AllData($sOqlUser) : DBObjectSearch::FromOQL($sOqlUser); - } catch (Exception $e) { - IssueLog::Error(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:AllUsers')).' - error details : '.$e->getMessage()); - throw new Exception(Dict::Format('Core:GetCountingUsers:Error', Dict::S('Core:CountingUsers:AllUsers')).'.'); - } - - return $this->GetUsersFromFilter($oFilter); - } - - /** - * @throws CoreException - * @throws MySQLException - * @throws CoreUnexpectedValue - * @throws OQLException - * @throws ArchivedObjectException - * @throws DictExceptionUnknownLanguage - */ - private function IsUserReadOnly(User $oUser, string $sClassCategory): bool - { - foreach (MetaModel::GetClasses($sClassCategory) as $sClass) { - // no need to check stimuli for now since users can't execute any without UR_ACTION_MODIFY - if ( - UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, null, $oUser) || - UserRights::IsActionAllowed($sClass, UR_ACTION_CREATE, null, $oUser) - ) { - return false; - } - } - return true; - } - - /** - * @throws CoreUnexpectedValue - * @throws CoreException - * @throws MySQLException - */ - public function GetUsersFromFilter(DBObjectSearch|DBUnionSearch|null $oFilter, array $aOrderBy = [], array $aArgs = []): array - { - $aUsers = []; - if (is_null($oFilter)) { - return $aUsers; - } - $oSet = new DBObjectSet($oFilter, $aOrderBy, $aArgs); - while ($oUser = $oSet->Fetch()) { - $aUsers[] = $oUser; - } - - return $aUsers; - } - - private function ArrayToOQLStringParameter(array $aValues): string - { - $aQuotedValues = []; - foreach ($aValues as $value) { - $value = trim((string) $value); - if ($value === '') { - continue; - } - $aQuotedValues[] = "'".addslashes($value)."'"; - } - - return empty($aQuotedValues) ? "''" : implode(', ', $aQuotedValues); - } - -} From 9c5bef05ef1a7c2258ede910440f92305534566e Mon Sep 17 00:00:00 2001 From: jf-cbd Date: Thu, 11 Jun 2026 16:25:20 +0200 Subject: [PATCH 22/22] Removing user counting from iTop (to put it in itop-system-information) --- .../unitary-tests/core/UserRightsTest.php | 100 ++++--- .../Users/ITopUserCountingRepositoryTest.php | 251 ------------------ 2 files changed, 47 insertions(+), 304 deletions(-) delete mode 100644 tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php index ec47012dba..5c286b91a5 100644 --- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php +++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php @@ -46,6 +46,13 @@ */ class UserRightsTest extends ItopDataTestCase { + public function setUp(): void + { + parent::setUp(); + + utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); + } + public static $aClasses = [ 'FunctionalCI' => ['class' => 'FunctionalCI', 'attcode' => 'name'], 'URP_UserProfile' => ['class' => 'URP_UserProfile', 'attcode' => 'reason'], @@ -54,11 +61,24 @@ class UserRightsTest extends ItopDataTestCase 'ModuleInstallation' => ['class' => 'ModuleInstallation', 'attcode' => 'name'], ]; - public function setUp(): void + /** + * @param string $sLoginPrefix + * @param int $iProfileId initial profile + * + * @return \DBObject + * @throws \CoreException + * @throws \Exception + */ + protected function CreateUniqueUserAndLogin(string $sLoginPrefix, int $iProfileId): DBObject { - parent::setUp(); + static $iCount = 0; + $sLogin = $sLoginPrefix.$iCount; + $iCount++; - utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); + $oUser = self::CreateUser($sLogin, $iProfileId); + $_SESSION = []; + UserRights::Login($sLogin); + return $oUser; } /** @@ -209,26 +229,19 @@ public function testIsActionAllowed(int $iProfileId, array $aClassActionResult) $this->assertEquals($aClassActionResult['res'], $bRes); } - /** - * @param string $sLoginPrefix - * @param int $iProfileId initial profile + /* + * FunctionalCI => bizmodel searchable + * UserRequest => bizmodel searchable requestmgmt + * URP_UserProfile => addon/userrights + * UserLocal => addon/authentication + * ModuleInstallation => core view_in_gui + * + * Profiles: + * 1 - Administrator + * 2 - User Portal + * 3 - Configuration manager * - * @return \DBObject - * @throws \CoreException - * @throws \Exception */ - protected function CreateUniqueUserAndLogin(string $sLoginPrefix, int $iProfileId): DBObject - { - static $iCount = 0; - $sLogin = $sLoginPrefix.$iCount; - $iCount++; - - $oUser = self::CreateUser($sLogin, $iProfileId); - $_SESSION = []; - UserRights::Login($sLogin); - return $oUser; - } - public function ActionAllowedProvider(): array { return [ @@ -276,20 +289,6 @@ public function ActionAllowedProvider(): array ]; } - /* - * FunctionalCI => bizmodel searchable - * UserRequest => bizmodel searchable requestmgmt - * URP_UserProfile => addon/userrights - * UserLocal => addon/authentication - * ModuleInstallation => core view_in_gui - * - * Profiles: - * 1 - Administrator - * 2 - User Portal - * 3 - Configuration manager - * - */ - /** Test IsActionAllowedOnAttribute * * @dataProvider ActionAllowedOnAttributeProvider @@ -309,6 +308,14 @@ public function testIsActionAllowedOnAttribute(int $iProfileId, array $aClassAct $this->assertEquals($aClassActionResult['res'], $bRes); } + /* + * FunctionalCI => bizmodel searchable + * UserRequest => bizmodel searchable requestmgmt + * URP_UserProfile => addon/userrights grant_by_profile + * UserLocal => addon/authentication grant_by_profile + * ModuleInstallation => core view_in_gui + * + */ public function ActionAllowedOnAttributeProvider(): array { return [ @@ -335,15 +342,6 @@ public function ActionAllowedOnAttributeProvider(): array ]; } - /* - * FunctionalCI => bizmodel searchable - * UserRequest => bizmodel searchable requestmgmt - * URP_UserProfile => addon/userrights grant_by_profile - * UserLocal => addon/authentication grant_by_profile - * ModuleInstallation => core view_in_gui - * - */ - /** * @dataProvider UserCannotLoseConsoleAccessProvider * @@ -487,7 +485,6 @@ public function testPrivilegedUsersMustHaveBackofficeAccess(int $iProfileId) $oUser->DBInsert(); } - public function PrivilegedUsersMustHaveBackofficeAccessProvider(): array { return [ @@ -497,7 +494,6 @@ public function PrivilegedUsersMustHaveBackofficeAccessProvider(): array ]; } - public function testNonPrivilegedUsersCanBeDeniedFromBackoffice() { $oUser = $this->GivenUserWithProfiles('test1', [5, 2]); @@ -533,7 +529,6 @@ public function NonAdminCanListOwnProfilesProvider(): array 'with Admins hidden' => [true], ]; } - /** * @dataProvider NonAdminCannotListAdminProfilesProvider */ @@ -580,12 +575,6 @@ public function testFindUser_ExistingInternalUser() ); } - public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) - { - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); - } - public function testFindUser_ExistingExternalUser() { $sLogin = 'AnExternalUser'.uniqid(); @@ -621,6 +610,11 @@ public function testFindUser_UnknownLogin() ); } + public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); + } public function FindUserAndAssertItWasNotFound($sLogin) { $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); diff --git a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php b/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php deleted file mode 100644 index 804d570a66..0000000000 --- a/tests/php-unit-tests/unitary-tests/sources/Users/ITopUserCountingRepositoryTest.php +++ /dev/null @@ -1,251 +0,0 @@ -CreateReadOnlyUsers(); - $this->CreateDisabledUsers(); - $this->CreatePortalUsers(); - if (MetaModel::IsValidClass('UserToken')) { - $this->CreateTokenUsers(); - } - $this->CreateConsoleUsers(); - $this->CreateBusinessPartnerUser(); - - } - - private function CreateReadOnlyUsers() - { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Service Catalog ReadOnly']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Portal user']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly', 'Portal user']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Service Catalog ReadOnly', 'Portal user']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly', 'Service Catalog ReadOnly']); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration ReadOnly', 'Ticket ReadOnly', 'Service Catalog ReadOnly']); - } - - private function CreateDisabledUsers() - { - $iDisabledUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], bReturnLogin: false); - $oUser = \MetaModel::GetObject('User', $iDisabledUser); - $oUser->Set('status', 'disabled'); - $oUser->DBUpdate(); - - $iDisabledReadOnlyUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Ticket ReadOnly'], bReturnLogin: false); - $oUser = \MetaModel::GetObject('User', $iDisabledReadOnlyUser); - $oUser->Set('status', 'disabled'); - $oUser->DBUpdate(); - } - - private function CreatePortalUsers() - { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], false); - - $iDisabledPortalUser = $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Portal user'], bReturnLogin: false); - $oUser = \MetaModel::GetObject('User', $iDisabledPortalUser); - $oUser->Set('status', 'disabled'); - $oUser->DBUpdate(); - } - - private function CreateTokenUsers() - { - $this->GivenTokenUserInDB(['Configuration Manager'], false); - $this->GivenTokenUserInDB(['Portal user'], false); - $this->GivenTokenUserInDB(['Configuration ReadOnly'], false); - } - - private function CreateConsoleUsers() - { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Configuration Manager'], bReturnLogin: false); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Administrator'], bReturnLogin: false); - } - - private function CreateBusinessPartnerUser() - { - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], bReturnLogin: false); - $this->GivenUserInDB('qpf_z17H3232*"ré$"é', ['Business partner user'], bReturnLogin: false); - } - - /** - * @throws \CoreUnexpectedValue - * @throws \DictExceptionMissingString - * @throws \CoreException - * @throws \MySQLException - * @throws \Exception - */ - public function testNotDuplicateInDifferentCountsCategories(): void - { - $oITopUserRepository = new ITopUserCountingRepository(); - - $aCountedUsers = [ - 'console' => $oITopUserRepository->GetConsoleUsers(), - 'portal' => $oITopUserRepository->GetPortalUsers(), - 'disabled' => $oITopUserRepository->GetDisabledUsers(), - 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), - 'businesspartner' => $oITopUserRepository->GetBusinessPartnerUsers(), - ]; - if (MetaModel::IsValidClass('UserToken')) { - $aCountedUsers['application'] = $oITopUserRepository->GetApplicationUsers(); - } - - $aCountedUserFormated = []; - foreach ($aCountedUsers as $sCountedCategory => $aUsers) { - foreach ($aUsers as $oUser) { - $sUserId = (string) $oUser->GetKey(); - $aCountedUserFormated[$sUserId][$sCountedCategory] = true; - } - } - - $aDuplicates = []; - foreach ($aCountedUserFormated as $sUserId => $aCountedCategory) { - $aCountedCategoryName = array_keys($aCountedCategory); - if (count($aCountedCategoryName) > 1) { - sort($aCountedCategoryName); - $aDuplicates[] = sprintf('User #%s appears in: %s', $sUserId, implode(', ', $aCountedCategoryName)); - } - } - - $this->assertEmpty( - $aDuplicates, - "Some users are counted in multiple categories:\n- ".implode("\n- ", $aDuplicates) - ); - } - - public function testAllUsersAreCounted() - { - $oITopUserRepository = new ITopUserCountingRepository(); - - $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); - $aPortalUsers = $oITopUserRepository->GetPortalUsers(); - $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); - $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; - $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); - - $aAllUsersFromMergedCounts = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); - - $aAllUsersFromOQL = $oITopUserRepository->GetAllUsers(); - - $this->assertEmpty( // asserts that all users are in both arrays - array_merge( - array_diff( - $aAllUsersFromMergedCounts, - $aAllUsersFromOQL - ), - array_diff( - $aAllUsersFromOQL, - $aAllUsersFromMergedCounts - ) - ) - ); - - // check for cardinality - $aMergedCountingUsersIds = []; - foreach ($aAllUsersFromMergedCounts as $oUser) { - $aMergedCountingUsersIds[] = (int) $oUser->GetKey(); - } - - $aOqlUsersIds = []; - foreach ($aAllUsersFromOQL as $oUser) { - $aOqlUsersIds[] = (int) $oUser->GetKey(); - } - - sort($aMergedCountingUsersIds, SORT_NUMERIC); - sort($aOqlUsersIds, SORT_NUMERIC); - - $this->assertSame($aOqlUsersIds, $aMergedCountingUsersIds); - } - - public function testAllCountedUsersAreUsersObjects() - { - $oITopUserRepository = new ITopUserCountingRepository(); - - $aConsoleUsers = $oITopUserRepository->GetConsoleUsers(); - $aPortalUsers = $oITopUserRepository->GetPortalUsers(); - $aDisabledUsers = $oITopUserRepository->GetDisabledUsers(); - $aReadOnlyUsers = $oITopUserRepository->GetReadOnlyUsers(); - $aApplicationUsers = $oITopUserRepository->GetApplicationUsers() ?? []; - $aBusinessPartnerUsers = $oITopUserRepository->GetBusinessPartnerUsers(); - - $aCountedUsers = array_merge($aConsoleUsers, $aPortalUsers, $aDisabledUsers, $aReadOnlyUsers, $aApplicationUsers, $aBusinessPartnerUsers); - - foreach ($aCountedUsers as $oUser) { - $this->assertInstanceOf(User::class, $oUser); - } - } - - /** - * @param array $aProfilesList - * @param $sExpectedCategorie - * - * @return void - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \DictExceptionMissingString - * @throws \MySQLException - * @dataProvider RealUseCasesDataProvider - */ - public function testRealUseCases(array $aProfilesList, $sExpectedCategorie) - { - $iUser = $this->GivenUserInDB('<_ç"éue"ç_u', $aProfilesList, bReturnLogin: false); - $oUser = \MetaModel::GetObject('User', $iUser); - \UserRights::Login($oUser->Get('login')); // work around since user needs to be logged in in IsActionAllowed - $oITopUserRepository = new ITopUserCountingRepository(); - $aUsers = match ($sExpectedCategorie) { - 'console' => $oITopUserRepository->GetConsoleUsers(), - 'readonly' => $oITopUserRepository->GetReadOnlyUsers(), - 'disabled' => $oITopUserRepository->GetDisabledUsers(), - 'portal' => $oITopUserRepository->GetPortalUsers(), - 'application' => $oITopUserRepository->GetApplicationUsers(), - 'businesspartner' => $oITopUserRepository->GetBusinessPartnerUsers(), - default => throw new \InvalidArgumentException(sprintf('Unexpected category %s', $sExpectedCategorie)), - }; - - $bUserInCorrectCategory = false; - foreach ($aUsers as $oUserInCategory) { - if ($oUserInCategory->GetKey() === $oUser->GetKey()) { - $bUserInCorrectCategory = true; - } - } - $this->assertTrue($bUserInCorrectCategory, 'User with profiles '.implode(', ', $aProfilesList).' should be counted as '.$sExpectedCategorie.' user'); - } - - public function RealUseCasesDataProvider() - { - return [ - 'User with profiles Support Agent and Configuration ReadOnly' => [ - 'profiles' => ['Support Agent', 'Configuration ReadOnly'], - 'expected category' => 'console', - ], - 'User with profiles Configuration ReadOnly and Service Catalog ReadOnly' => [ - 'profiles' => ['Configuration ReadOnly', 'Service Catalog ReadOnly'], - 'expected category' => 'readonly', - ], - 'User with profiles Portal user and Service Catalog ReadOnly' => [ - 'profiles' => ['Portal user', 'Service Catalog ReadOnly'], - 'expected category' => 'portal', - ], - 'User with profiles Support Agent and Portal user' => [ - 'profiles' => ['Support Agent', 'Portal user'], - 'expected category' => 'portal', - ], - 'User with profiles Configuration Manager and Ticket ReadOnly' => [ - 'profiles' => ['Configuration Manager', 'Ticket ReadOnly'], - 'expected category' => 'console', - ], - ]; - } -}