mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-23 06:38:19 +02:00
Compare commits
223 Commits
feature/96
...
issue/9954
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe705c70d | ||
|
|
3987785690 | ||
|
|
b8f02f48bd | ||
|
|
ba426a9602 | ||
|
|
285f64f8db | ||
|
|
be5a5b0da1 | ||
|
|
7583ede3b3 | ||
|
|
95cb21cff7 | ||
|
|
cdd7bbedcb | ||
|
|
ea1ed03d0c | ||
|
|
2073e8c10c | ||
|
|
1a41349d7b | ||
|
|
fdc1ef40e6 | ||
|
|
4722551a16 | ||
|
|
a2934ef05b | ||
|
|
1cdc2c5648 | ||
|
|
31eddad029 | ||
|
|
b43d71f324 | ||
|
|
26033774b8 | ||
|
|
27c0fe191b | ||
|
|
fac132f707 | ||
|
|
6f9a4cb55f | ||
|
|
c07317dc71 | ||
|
|
a3fcc6fdf0 | ||
|
|
b13065f23f | ||
|
|
2eaca40632 | ||
|
|
7f3a1811d7 | ||
|
|
87b48c638c | ||
|
|
54a1221e4a | ||
|
|
97c589c440 | ||
|
|
68e799c0fd | ||
|
|
658825b308 | ||
|
|
af08824866 | ||
|
|
8693951aa9 | ||
|
|
a28e481ee4 | ||
|
|
6032628a70 | ||
|
|
2892949e33 | ||
|
|
ba643ba2ab | ||
|
|
3711e8a5a1 | ||
|
|
a38fef5bbf | ||
|
|
23e4ce7263 | ||
|
|
1bba486ee8 | ||
|
|
801090fddb | ||
|
|
1d77b9faa2 | ||
|
|
5282913791 | ||
|
|
36fae27355 | ||
|
|
812f0b573d | ||
|
|
c7c16f091b | ||
|
|
3739067473 | ||
|
|
fd553b86dd | ||
|
|
d7625d9650 | ||
|
|
893a1d7f71 | ||
|
|
fda1d3dd16 | ||
|
|
61464fc819 | ||
|
|
c281677768 | ||
|
|
e280251623 | ||
|
|
f657308137 | ||
|
|
4596404666 | ||
|
|
64394099f7 | ||
|
|
67e2899a60 | ||
|
|
8d75531d95 | ||
|
|
1e023e5431 | ||
|
|
e2a1c3e79e | ||
|
|
c7570d62c1 | ||
|
|
a30ed63a62 | ||
|
|
dd77124b07 | ||
|
|
9bfe0430f5 | ||
|
|
3fe91ed0b4 | ||
|
|
ccb9633226 | ||
|
|
82b50dc6cd | ||
|
|
d3451f8c30 | ||
|
|
b5c8a98695 | ||
|
|
1403914572 | ||
|
|
b976471a0d | ||
|
|
fa66a09104 | ||
|
|
47981f78e3 | ||
|
|
476ec75a2e | ||
|
|
10c000ddfe | ||
|
|
7e55b9ccfc | ||
|
|
5bb288ffca | ||
|
|
56d7e40f4c | ||
|
|
e0dc1fa571 | ||
|
|
d6466c0bd6 | ||
|
|
eb16e6cbc8 | ||
|
|
42007ffd95 | ||
|
|
6329c157e8 | ||
|
|
9ee0548767 | ||
|
|
620a6b9a51 | ||
|
|
968c56745a | ||
|
|
04a5e5463a | ||
|
|
006dd3d6c2 | ||
|
|
a808e89c30 | ||
|
|
c6e50a0ed7 | ||
|
|
d88687527e | ||
|
|
2cc071b07a | ||
|
|
76a410e00d | ||
|
|
c68887c2bc | ||
|
|
837b433622 | ||
|
|
52fca750b0 | ||
|
|
a5c14e4870 | ||
|
|
0e72b090ee | ||
|
|
d1aedee245 | ||
|
|
1b70b4bf70 | ||
|
|
41f51a2c0c | ||
|
|
73ede13443 | ||
|
|
40dd72ef82 | ||
|
|
c8aea56171 | ||
|
|
041437cbeb | ||
|
|
85d50f31bd | ||
|
|
5715e0484f | ||
|
|
109fdfa06b | ||
|
|
3c301affb3 | ||
|
|
147fae7f8c | ||
|
|
a5aa8c279f | ||
|
|
13fad9efc1 | ||
|
|
9fbc53cf00 | ||
|
|
04f91ab4a2 | ||
|
|
918f110dc9 | ||
|
|
8e0246601a | ||
|
|
bd9cc9c15e | ||
|
|
60ce21509f | ||
|
|
e6cdcd0a5a | ||
|
|
2ab3081926 | ||
|
|
5cabac63e7 | ||
|
|
3918dcf830 | ||
|
|
489300552a | ||
|
|
8507c606ba | ||
|
|
128989450b | ||
|
|
1bd404af6f | ||
|
|
6c770d3e16 | ||
|
|
a74dbb3a73 | ||
|
|
b365aa0971 | ||
|
|
56aa50c9b1 | ||
|
|
eacb7f3401 | ||
|
|
b14045af29 | ||
|
|
7321eeb23f | ||
|
|
05b8c529fe | ||
|
|
a68c017225 | ||
|
|
82b52b95a1 | ||
|
|
556f612f7e | ||
|
|
edc69bebba | ||
|
|
dce1994b85 | ||
|
|
fde6527057 | ||
|
|
0de0592c18 | ||
|
|
c9d512cc31 | ||
|
|
a3ef049063 | ||
|
|
87ec702c5e | ||
|
|
127be1933d | ||
|
|
2c14030fc5 | ||
|
|
7825c08069 | ||
|
|
4ddf17233f | ||
|
|
9493bb03d5 | ||
|
|
8f87c759b5 | ||
|
|
348c90145d | ||
|
|
c37143176a | ||
|
|
0c8229a68e | ||
|
|
fda5fe5ded | ||
|
|
6b396adeea | ||
|
|
c448f8411c | ||
|
|
4b4fe55060 | ||
|
|
dedcd4af19 | ||
|
|
0556a03839 | ||
|
|
64df429715 | ||
|
|
9ae813db2b | ||
|
|
768423a6aa | ||
|
|
921e7c88fe | ||
|
|
d6ef4fb7bb | ||
|
|
a335e1004b | ||
|
|
e882df96d9 | ||
|
|
3a7439e970 | ||
|
|
7fca6424ab | ||
|
|
f09a73c45a | ||
|
|
5a174dd8fa | ||
|
|
01f32086ec | ||
|
|
5c59fdebaf | ||
|
|
363d0767d5 | ||
|
|
5202adb902 | ||
|
|
f77dd7b668 | ||
|
|
f1af3d5c5f | ||
|
|
614d3586c8 | ||
|
|
f9dc3ee73a | ||
|
|
1d9e7f9b55 | ||
|
|
99a9c8ef40 | ||
|
|
ff2357473b | ||
|
|
9a93bcb06f | ||
|
|
9b4f3290e8 | ||
|
|
67792155dd | ||
|
|
a23b9aa596 | ||
|
|
017481af21 | ||
|
|
4f70441618 | ||
|
|
fb6a332bd0 | ||
|
|
a9af4fc060 | ||
|
|
6a8dc159c1 | ||
|
|
2930a122c6 | ||
|
|
75433d3390 | ||
|
|
d4d6fa149d | ||
|
|
207ac373d0 | ||
|
|
0240d12007 | ||
|
|
1123a83490 | ||
|
|
8ccf832bdd | ||
|
|
caa1b8652d | ||
|
|
ed1cfc3b08 | ||
|
|
dd6b03cccc | ||
|
|
3e38e349e5 | ||
|
|
65b58ec4e2 | ||
|
|
6a20e36434 | ||
|
|
3805a322c1 | ||
|
|
0bf773a23c | ||
|
|
28a09068a0 | ||
|
|
db7fd6b3c0 | ||
|
|
cf76c23ac6 | ||
|
|
b36b0de93c | ||
|
|
a95125e8bf | ||
|
|
6e536e7dbe | ||
|
|
733eea8a78 | ||
|
|
f48efbbd03 | ||
|
|
1a57a427c0 | ||
|
|
a419914236 | ||
|
|
3426b92768 | ||
|
|
7c44c94de1 | ||
|
|
4371ddfbbd | ||
|
|
66bf1dc6fa | ||
|
|
6999000d69 |
@@ -2,13 +2,17 @@
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'themeVariables': {
|
||||
'git0': 'lawngreen',
|
||||
'git3': 'dodgerblue',
|
||||
'git4': 'grey',
|
||||
'git5': 'grey',
|
||||
'git6': 'grey',
|
||||
'git7': 'grey',
|
||||
'git8': 'grey'
|
||||
'git0': '#7ccf00',
|
||||
'git1': '#7ccf00',
|
||||
'git2': '#99a1af',
|
||||
'git3': '#99a1af',
|
||||
'git4': '#99a1af',
|
||||
'git5': '#99a1af',
|
||||
'git6': '#99a1af',
|
||||
'git7': '#99a1af',
|
||||
'git8': '#99a1af',
|
||||
'git9': '#99a1af',
|
||||
'git10': '#99a1af'
|
||||
}, 'gitGraph': {'showBranches': true,'mainBranchName': 'develop','rotateCommitLabel': true}} }%%
|
||||
gitGraph
|
||||
commit id: "2016-07-06" tag: "2.3.0" type: HIGHLIGHT
|
||||
@@ -106,6 +110,11 @@ gitGraph
|
||||
commit id: "2025-09-25" tag: "2.7.13"
|
||||
checkout support/3.2
|
||||
commit id: "2026-04-27 " tag: "3.2.3"
|
||||
checkout support/3.2.3
|
||||
commit id: "2026-05-25 " tag: "3.2.3-1"
|
||||
commit id: "2026-07-17 " tag: "3.2.3-2"
|
||||
checkout develop
|
||||
commit id: "2026-07-23" tag: "3.3.0-beta1"
|
||||
```
|
||||
|
||||
To learn more, check the [iTop community versions history on the official wiki](https://www.itophub.io/wiki/page?id=latest:release:start).
|
||||
|
||||
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -9,11 +9,6 @@ Any PRs not following the guidelines or with missing information will not be con
|
||||
|
||||
## Base information
|
||||
|
||||
| Question | Answer
|
||||
|----------------------------------------------------------------|--------
|
||||
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
|
||||
| Type of change? | Bug fix / Enhancement / Translations
|
||||
|
||||
| Question | Answer |
|
||||
|---------------------------------------------------------------------------------|--------------------------------------|
|
||||
| Related to a SourceForge thread / Another PR / A GitHub Issue / Combodo ticket? | <!-- Put the URL --> |
|
||||
|
||||
0
.make/license/gen-community-license.sh
Normal file → Executable file
0
.make/license/gen-community-license.sh
Normal file → Executable file
0
.make/license/sortLicenceXml.php
Normal file → Executable file
0
.make/license/sortLicenceXml.php
Normal file → Executable file
0
.make/license/updateLicenses.php
Normal file → Executable file
0
.make/license/updateLicenses.php
Normal file → Executable file
0
.make/release/changelog.php
Normal file → Executable file
0
.make/release/changelog.php
Normal file → Executable file
0
.make/release/update-versions.php
Normal file → Executable file
0
.make/release/update-versions.php
Normal file → Executable file
0
.make/release/update-xml.php
Normal file → Executable file
0
.make/release/update-xml.php
Normal file → Executable file
0
.make/release/update.classes.inc.php
Normal file → Executable file
0
.make/release/update.classes.inc.php
Normal file → Executable file
@@ -582,16 +582,30 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
*/
|
||||
public function ListProfiles($oUser)
|
||||
{
|
||||
$aRet = [];
|
||||
$oSearch = new DBObjectSearch('URP_UserProfile');
|
||||
$oSearch->AllowAllData();
|
||||
$oSearch->NoContextParameters();
|
||||
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
|
||||
$oProfiles = new DBObjectSet($oSearch);
|
||||
while ($oUserProfile = $oProfiles->Fetch()) {
|
||||
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
|
||||
if (!array_key_exists('profile_list', $oUser->ListChanges())) {
|
||||
// Profiles list is not modified on the $oUser object, we can use DBObjectSearch with `all data` capabilities
|
||||
// Note: This is the default behavior
|
||||
$aRet = [];
|
||||
$oSearch = new DBObjectSearch('URP_UserProfile');
|
||||
$oSearch->AllowAllData();
|
||||
$oSearch->NoContextParameters();
|
||||
$oSearch->Addcondition('userid', $oUser->GetKey(), '=');
|
||||
$oProfiles = new DBObjectSet($oSearch);
|
||||
while ($oUserProfile = $oProfiles->Fetch()) {
|
||||
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
|
||||
}
|
||||
|
||||
return $aRet;
|
||||
} else {
|
||||
// Profiles list must be computed with memory changes.
|
||||
// Note: this is a bit tricky because the user object may have been modified in memory (e.g. a profile added or removed) and we need to take that into account
|
||||
$aRet = [];
|
||||
$oProfilesSet = $oUser->Get('profile_list');
|
||||
foreach ($oProfilesSet as $oUserProfile) {
|
||||
$aRet[$oUserProfile->Get('profileid')] = $oUserProfile->Get('profileid_friendlyname');
|
||||
}
|
||||
return $aRet;
|
||||
}
|
||||
return $aRet;
|
||||
}
|
||||
|
||||
public function GetSelectFilter($oUser, $sClass, $aSettings = [])
|
||||
@@ -705,26 +719,23 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
protected function GetUserActionGrant($oUser, $sClass, $iActionCode)
|
||||
{
|
||||
$this->LoadCache();
|
||||
|
||||
// load and cache permissions for the current user on the given class
|
||||
//
|
||||
$iUser = $oUser->GetKey();
|
||||
if (isset($this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode])) {
|
||||
$aTest = $this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode];
|
||||
if (is_array($aTest)) {
|
||||
return $aTest;
|
||||
if (count($oUser->ListChanges()) === 0) {
|
||||
// load and cache permissions for the current user on the given class
|
||||
if (isset($this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode])) {
|
||||
$aTest = $this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode];
|
||||
if (is_array($aTest)) {
|
||||
return $aTest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sAction = self::$m_aActionCodes[$iActionCode];
|
||||
|
||||
$bStatus = null;
|
||||
// Cache user's profiles
|
||||
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
|
||||
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
|
||||
}
|
||||
|
||||
$aProfileList = $this->GetProfileList($oUser);
|
||||
// Call the API of UserRights because it caches the list for us
|
||||
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
|
||||
foreach ($aProfileList as $iProfile => $oProfile) {
|
||||
$bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction);
|
||||
if (!is_null($bGrant)) {
|
||||
if ($bGrant) {
|
||||
@@ -742,7 +753,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
$aRes = [
|
||||
'permission' => $iPermission,
|
||||
];
|
||||
$this->m_aObjectActionGrants[$iUser][$sClass][$iActionCode] = $aRes;
|
||||
if (count($oUser->ListChanges()) === 0) {
|
||||
$this->m_aObjectActionGrants[$oUser->GetKey()][$sClass][$iActionCode] = $aRes;
|
||||
}
|
||||
return $aRes;
|
||||
}
|
||||
|
||||
@@ -824,18 +837,14 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
{
|
||||
$this->LoadCache();
|
||||
// Note: this code is VERY close to the code of IsActionAllowed()
|
||||
$iUser = $oUser->GetKey();
|
||||
|
||||
// Cache user's profiles
|
||||
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
|
||||
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
|
||||
}
|
||||
$aProfileList = $this->GetProfileList($oUser);
|
||||
|
||||
// Note: The object set is ignored because it was interesting to optimize for huge data sets
|
||||
// and acceptable to consider only the root class of the object set
|
||||
$bStatus = null;
|
||||
// Call the API of UserRights because it caches the list for us
|
||||
foreach ($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile) {
|
||||
foreach ($aProfileList as $iProfile => $oProfile) {
|
||||
$bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode);
|
||||
if (!is_null($bGrant)) {
|
||||
if ($bGrant) {
|
||||
@@ -893,6 +902,25 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
}
|
||||
return $bHasSharing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \User $oUser
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetProfileList(User $oUser): array
|
||||
{
|
||||
if (count($oUser->ListChanges()) === 0) { // if user is already in db and not changed
|
||||
$iUser = $oUser->GetKey();
|
||||
if (false === array_key_exists($iUser, $this->aUsersProfilesList)) {
|
||||
$aProfiles = UserRights::ListProfiles($oUser);
|
||||
$this->aUsersProfilesList[$iUser] = $aProfiles;
|
||||
}
|
||||
return $this->aUsersProfilesList[$iUser];
|
||||
}
|
||||
return UserRights::ListProfiles($oUser);
|
||||
}
|
||||
}
|
||||
|
||||
UserRights::SelectModule('UserRightsProfile');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implement this interface to add sass file (SCSS) to the backoffice pages.
|
||||
* Implement this interface to add sass files (SCSS) to the backoffice pages.
|
||||
* example: return "css/setup.scss"
|
||||
*
|
||||
* @api
|
||||
@@ -11,9 +11,9 @@
|
||||
interface iBackofficeSassExtension
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
* @see \iTopWebPage::$a_styles
|
||||
* @return array An array of relative paths (from loaded import paths) to the files to compile and include
|
||||
* @see \iTopWebPage::$a_linked_stylesheets
|
||||
* @api
|
||||
*/
|
||||
public function GetSass(): string;
|
||||
public function GetSassRelPaths(): array;
|
||||
}
|
||||
|
||||
@@ -97,33 +97,125 @@ class RestUtils
|
||||
* @throws Exception
|
||||
* @api
|
||||
*/
|
||||
public static function GetFieldList($sClass, $oData, $sParamName)
|
||||
public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
|
||||
{
|
||||
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
|
||||
$aShowFields = [];
|
||||
if ($sFields == '*') {
|
||||
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
|
||||
$aShowFields[$sClass][] = $sAttCode;
|
||||
}
|
||||
} elseif ($sFields == '*+') {
|
||||
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
|
||||
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) {
|
||||
$aShowFields[$sRefClass][] = $sAttCode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach (explode(',', $sFields) as $sAttCode) {
|
||||
$sAttCode = trim($sAttCode);
|
||||
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) {
|
||||
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
|
||||
}
|
||||
$aShowFields[$sClass][] = $sAttCode;
|
||||
}
|
||||
}
|
||||
|
||||
return $aShowFields;
|
||||
return match($sFields) {
|
||||
'*' => self::GetFieldListForClass($sClass),
|
||||
'*+' => self::GetFieldListForParentClass($sClass),
|
||||
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the requested field list asks for an extended output.
|
||||
*
|
||||
* Extended output is requested when using '*+' or class-scoped field definitions.
|
||||
*
|
||||
* @param string $sFields Requested field specification.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function HasRequestedExtendedOutput(string $sFields): bool
|
||||
{
|
||||
return match($sFields) {
|
||||
'*' => false,
|
||||
'*+' => true,
|
||||
default => substr_count($sFields, ':') > 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the requested field list asks for all output fields.
|
||||
*
|
||||
* @param string $sFields Requested field specification.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function HasRequestedAllOutputFields(string $sFields): bool
|
||||
{
|
||||
return match($sFields) {
|
||||
'*', '*+' => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
protected static function GetFieldListForClass(string $sClass): array
|
||||
{
|
||||
return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a field list for all child classes of the given parent class.
|
||||
*
|
||||
* @param string $sClass Parent class name.
|
||||
*
|
||||
* @return array Array of class => list of attribute codes.
|
||||
*/
|
||||
protected static function GetFieldListForParentClass(string $sClass): array
|
||||
{
|
||||
$aFieldList = [];
|
||||
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
|
||||
$aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
|
||||
}
|
||||
return $aFieldList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a restricted field list for one class from a comma-separated attribute list.
|
||||
*
|
||||
* @param string $sClass Class name.
|
||||
* @param string $sFields Comma-separated list of requested attribute codes.
|
||||
* @param string $sParamName Input parameter name used in error messages.
|
||||
* @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
|
||||
*
|
||||
* @return array Array containing one class => list of attribute codes.
|
||||
* @throws Exception When an attribute code is invalid and $bFailIfNotFound is true.
|
||||
*/
|
||||
protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
|
||||
{
|
||||
$aFieldList = [$sClass => []];
|
||||
foreach (explode(',', $sFields) as $sAttCode) {
|
||||
$sAttCode = trim($sAttCode);
|
||||
if (($sAttCode == 'id') || (MetaModel::IsValidAttCode($sClass, $sAttCode))) {
|
||||
$aFieldList[$sClass][] = $sAttCode;
|
||||
} else {
|
||||
if ($bFailIfNotFound) {
|
||||
throw new Exception("$sParamName: invalid attribute code '$sAttCode' for class '$sClass'");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $aFieldList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a restricted field list for one or several classes.
|
||||
*
|
||||
* Accepted formats are either "att1,att2" for a single class, or
|
||||
* "ClassA:att1,att2;ClassB:att3" for class-scoped field definitions.
|
||||
*
|
||||
* @param string $sClass Default class name used when no class scope is specified.
|
||||
* @param string $sFields Requested field specification.
|
||||
* @param string $sParamName Input parameter name used in error messages.
|
||||
* @param bool $bFailIfNotFound If true, throws when an attribute code is invalid.
|
||||
*
|
||||
* @return array Array of class => list of attribute codes.
|
||||
* @throws Exception Propagated from GetLimitedFieldListForSingleClass.
|
||||
*/
|
||||
protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
|
||||
{
|
||||
if (!str_contains($sFields, ':')) {
|
||||
return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName, $bFailIfNotFound);
|
||||
}
|
||||
|
||||
$aFieldList = [];
|
||||
$aFieldListParts = explode(';', $sFields);
|
||||
foreach ($aFieldListParts as $sClassFields) {
|
||||
list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
|
||||
$aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName, $bFailIfNotFound));
|
||||
}
|
||||
return $aFieldList;
|
||||
}
|
||||
/**
|
||||
* Read and interpret object search criteria from a Rest/Json structure
|
||||
*
|
||||
|
||||
@@ -175,6 +175,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
|
||||
*/
|
||||
protected $sDisplayMode;
|
||||
protected $aFieldsMap;
|
||||
/**
|
||||
* @var array Store posted values in order to be used in GetAttributeFlags
|
||||
* @since 3.3.0
|
||||
*/
|
||||
protected $aPostedValues = [];
|
||||
|
||||
/**
|
||||
* If true, bypass IsActionAllowedOnAttribute when writing this object
|
||||
@@ -524,6 +529,12 @@ JS
|
||||
$sLabel = Dict::S('Tag:Synchronized');
|
||||
$sSynchroTagId = 'synchro_icon-'.$this->GetKey();
|
||||
$aTags[$sSynchroTagId] = ['title' => $sTip, 'css_classes' => 'ibo-object-details--tag--synchronized', 'decoration_classes' => 'fas fa-lock', 'label' => $sLabel];
|
||||
if (UserRights::IsActionAllowed(SynchroReplica::class, UR_ACTION_READ)) {
|
||||
$oDBSearch = DBObjectSearch::FromOQL('SELECT SynchroReplica WHERE dest_class=:sClass AND dest_id=:id');
|
||||
$sFilter = rawurlencode($oDBSearch->serialize(false, ['sClass' => get_class($this),'id' => $this->GetKey()]));
|
||||
$sUrlSearchReplica = 'UI.php?operation=search&filter='.$sFilter;
|
||||
$oPage->add_ready_script("$('#$sSynchroTagId').on('click',function() {window.location = '$sUrlSearchReplica' });");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,7 +1046,7 @@ HTML
|
||||
// Add extra data for markup generation
|
||||
// - Attribute code and AttributeDef. class
|
||||
$val['attcode'] = $sAttCode;
|
||||
$val['atttype'] = $oAttDef->GetType();
|
||||
$val['atttype'] = $oAttDef->GetTypeShortClassName();
|
||||
$val['attlabel'] = $sAttLabel;
|
||||
$val['attflags'] = ($bEditMode) ? $this->GetFormAttributeFlags($sAttCode) : OPT_ATT_READONLY;
|
||||
|
||||
@@ -1524,190 +1535,6 @@ HTML
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param \CMDBObjectSet $oSet
|
||||
* @param array $aParams
|
||||
*
|
||||
* @throws \Exception
|
||||
* only used in old and deprecated export.php
|
||||
*
|
||||
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
|
||||
*/
|
||||
public static function DisplaySetAsHTMLSpreadsheet(WebPage $oPage, CMDBObjectSet $oSet, $aParams = [])
|
||||
{
|
||||
$oPage->add(self::GetSetAsHTMLSpreadsheet($oSet, $aParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreadsheet output: designed for end users doing some reporting
|
||||
* Then the ids are excluded and replaced by the corresponding friendlyname
|
||||
*
|
||||
* @param \DBObjectSet $oSet
|
||||
* @param array $aParams
|
||||
*
|
||||
* @return string
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \Exception
|
||||
*
|
||||
* @internal Only to be used by `/webservices/export.php` : this is a legacy method that produces wrong HTML (no TR on table body rows)
|
||||
*/
|
||||
public static function GetSetAsHTMLSpreadsheet(DBObjectSet $oSet, $aParams = [])
|
||||
{
|
||||
$aFields = null;
|
||||
if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0)) {
|
||||
$aFields = explode(',', $aParams['fields']);
|
||||
}
|
||||
|
||||
$bFieldsAdvanced = false;
|
||||
if (isset($aParams['fields_advanced'])) {
|
||||
$bFieldsAdvanced = (bool)$aParams['fields_advanced'];
|
||||
}
|
||||
|
||||
$bLocalize = true;
|
||||
if (isset($aParams['localize_values'])) {
|
||||
$bLocalize = (bool)$aParams['localize_values'];
|
||||
}
|
||||
|
||||
$aList = [];
|
||||
|
||||
$aClasses = $oSet->GetFilter()->GetSelectedClasses();
|
||||
$aAuthorizedClasses = [];
|
||||
foreach ($aClasses as $sAlias => $sClassName) {
|
||||
if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO) {
|
||||
$aAuthorizedClasses[$sAlias] = $sClassName;
|
||||
}
|
||||
}
|
||||
$aHeader = [];
|
||||
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
|
||||
$aList[$sAlias] = [];
|
||||
|
||||
foreach (MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef) {
|
||||
if (is_null($aFields) || (count($aFields) == 0)) {
|
||||
// Standard list of attributes (no link sets)
|
||||
if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) {
|
||||
$sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
|
||||
|
||||
$aList[$sAlias][$sAttCodeEx] = $oAttDef;
|
||||
|
||||
if ($bFieldsAdvanced && $oAttDef->IsExternalKey(EXTKEY_RELATIVE)) {
|
||||
$sRemoteClass = $oAttDef->GetTargetClass();
|
||||
foreach (MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode) {
|
||||
$aList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef(
|
||||
$sRemoteClass,
|
||||
$sRemoteAttCode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User defined list of attributes
|
||||
if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields)) {
|
||||
$aList[$sAlias][$sAttCode] = $oAttDef;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Replace external key by the corresponding friendly name (if not already in the list)
|
||||
foreach ($aList[$sAlias] as $sAttCode => $oAttDef) {
|
||||
if ($oAttDef->IsExternalKey()) {
|
||||
unset($aList[$sAlias][$sAttCode]);
|
||||
$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
|
||||
if (!array_key_exists(
|
||||
$sFriendlyNameAttCode,
|
||||
$aList[$sAlias]
|
||||
) && MetaModel::IsValidAttCode($sClassName, $sFriendlyNameAttCode)) {
|
||||
$oFriendlyNameAtt = MetaModel::GetAttributeDef($sClassName, $sFriendlyNameAttCode);
|
||||
$aList[$sAlias][$sFriendlyNameAttCode] = $oFriendlyNameAtt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
|
||||
$sColLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx) : $sAttCodeEx;
|
||||
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
|
||||
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Date').')';
|
||||
$aHeader[] = $sColLabel.' ('.Dict::S('UI:SplitDateTime-Time').')';
|
||||
} else {
|
||||
$aHeader[] = $sColLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sHtml = "<table border=\"1\">\n";
|
||||
$sHtml .= "<tr>\n";
|
||||
$sHtml .= "<td>".implode("</td><td>", $aHeader)."</td>\n";
|
||||
$sHtml .= "</tr>\n";
|
||||
$oSet->Seek(0);
|
||||
while ($aObjects = $oSet->FetchAssoc()) {
|
||||
$aRow = [];
|
||||
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
|
||||
$oObj = $aObjects[$sAlias];
|
||||
foreach ($aList[$sAlias] as $sAttCodeEx => $oAttDef) {
|
||||
if (is_null($oObj)) {
|
||||
$aRow[] = '<td></td>';
|
||||
} else {
|
||||
$oFinalAttDef = $oAttDef->GetFinalAttDef();
|
||||
if (get_class($oFinalAttDef) == 'AttributeDateTime') {
|
||||
$sDate = $oObj->Get($sAttCodeEx);
|
||||
if ($sDate === null) {
|
||||
$aRow[] = '<td></td>';
|
||||
$aRow[] = '<td></td>';
|
||||
} else {
|
||||
$iDate = AttributeDateTime::GetAsUnixSeconds($sDate);
|
||||
$aRow[] = '<td>'.date(
|
||||
'Y-m-d',
|
||||
$iDate
|
||||
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
|
||||
$aRow[] = '<td>'.date(
|
||||
'H:i:s',
|
||||
$iDate
|
||||
).'</td>'; // Format kept as-is for 100% backward compatibility of the exports
|
||||
}
|
||||
} else {
|
||||
if ($oAttDef instanceof AttributeCaseLog) {
|
||||
$rawValue = $oObj->Get($sAttCodeEx);
|
||||
$outputValue = str_replace(
|
||||
"\n",
|
||||
"<br/>",
|
||||
utils::EscapeHtml($rawValue->__toString())
|
||||
);
|
||||
// Trick for Excel: treat the content as text even if it begins with an equal sign
|
||||
$aRow[] = '<td x:str>'.$outputValue.'</td>';
|
||||
} else {
|
||||
$rawValue = $oObj->Get($sAttCodeEx);
|
||||
// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
|
||||
// let's fix this and make sure we render an empty string if the key == 0
|
||||
if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) {
|
||||
$sKeyAttCode = $oAttDef->GetKeyAttCode();
|
||||
if ($oObj->Get($sKeyAttCode) == 0) {
|
||||
$rawValue = '';
|
||||
}
|
||||
}
|
||||
if ($bLocalize) {
|
||||
$outputValue = utils::EscapeHtml($oFinalAttDef->GetEditValue($rawValue));
|
||||
} else {
|
||||
$outputValue = utils::EscapeHtml($rawValue);
|
||||
}
|
||||
$aRow[] = '<td>'.$outputValue.'</td>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$sHtml .= implode("\n", $aRow);
|
||||
$sHtml .= "</tr>\n";
|
||||
}
|
||||
$sHtml .= "</table>\n";
|
||||
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param \CMDBObjectSet $oSet
|
||||
@@ -3239,8 +3066,12 @@ EOF
|
||||
}
|
||||
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
|
||||
if (is_array($aAllowedValues) && count($aAllowedValues) == 1) {
|
||||
$aValues = array_keys($aAllowedValues);
|
||||
$this->Set($sAttCode, $aValues[0]);
|
||||
// linkset are ignored
|
||||
// N°9622 - Error in the user story life cycle
|
||||
if (!$oAttDef->IsLinkSet()) {
|
||||
$aValues = array_keys($aAllowedValues);
|
||||
$this->Set($sAttCode, $aValues[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3986,6 +3817,7 @@ HTML;
|
||||
|
||||
$aErrors = [];
|
||||
$aFinalValues = [];
|
||||
$this->aPostedValues = $aValues; // Store the values for later use (e.g. in GetAttributeFlags)
|
||||
foreach ($this->GetWriteableAttList(array_keys($aValues), $aErrors, $aAttFlags) as $sAttCode => $oAttDef) {
|
||||
$aFinalValues[$sAttCode] = $aValues[$sAttCode];
|
||||
}
|
||||
@@ -4155,6 +3987,27 @@ HTML;
|
||||
$sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');
|
||||
if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled
|
||||
$value = json_decode($sTagSetJson, true);
|
||||
if ($this->IsNew()) {
|
||||
if (is_array($value['orig_value'])) {
|
||||
foreach ($value['orig_value'] as $val) {
|
||||
if (!in_array($val, $value['removed'])) {
|
||||
$value['added'][] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$aCurrentValues = $this->Get($sAttCode)->GetValues();
|
||||
foreach ($value['orig_value'] as $val) {
|
||||
if (!in_array($val, $aCurrentValues) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
|
||||
$value['added'][] = $val;
|
||||
}
|
||||
}
|
||||
foreach ($aCurrentValues as $val) {
|
||||
if (!in_array($val, $value['orig_value']) && !in_array($val, $value['removed']) && !in_array($val, $value['added'])) {
|
||||
$value['removed'][] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -4495,7 +4348,7 @@ HTML;
|
||||
|
||||
$oDivField = FieldUIBlockFactory::MakeLarge("");
|
||||
// UIContentBlockUIBlockFactory::MakeStandard(null,["field_container field_large"]);
|
||||
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetType());
|
||||
$oDivField->AddDataAttribute("attribute-type", $oAttDef->GetTypeShortClassName());
|
||||
$oDivField->AddDataAttribute("attribute-label", $sAttMetaDataLabel);
|
||||
$oDivField->AddDataAttribute("attribute-flag-hidden", false);
|
||||
$oDivField->AddDataAttribute("attribute-flag-read-only", false);
|
||||
|
||||
@@ -501,7 +501,7 @@ EOF
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
|
||||
{
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? $this->GetId(), $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
|
||||
/** @var \DashboardLayoutMultiCol $oLayout */
|
||||
$oLayout = new $this->sLayoutClass();
|
||||
|
||||
@@ -25,6 +25,20 @@ use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\iUIBlock;
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDateTime;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDecimal;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeDuration;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeEncryptedString;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeEnum;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeInteger;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeOneWayPassword;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributePassword;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributePercentage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
|
||||
use Combodo\iTop\Core\AttributeDefinition\iAttributeNoGroupBy;
|
||||
|
||||
require_once(APPROOT.'application/forms.class.inc.php');
|
||||
|
||||
@@ -440,7 +454,7 @@ EOF
|
||||
// For external fields, find the real type of the target
|
||||
$sExtFieldAttCode = $sAttCode;
|
||||
$sTargetClass = $sClass;
|
||||
while (is_a($sAttType, 'AttributeExternalField', true)) {
|
||||
while (is_a($sAttType, AttributeExternalField::class, true)) {
|
||||
$sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
|
||||
$sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
|
||||
$sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
|
||||
@@ -450,13 +464,13 @@ EOF
|
||||
}
|
||||
|
||||
$aForbidenAttType = [
|
||||
'AttributeLinkedSet',
|
||||
'AttributeFriendlyName',
|
||||
AttributeLinkedSet::class,
|
||||
AttributeFriendlyName::class,
|
||||
|
||||
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
|
||||
'AttributeOneWayPassword',
|
||||
'AttributeEncryptedString',
|
||||
'AttributePassword',
|
||||
iAttributeNoGroupBy::class, //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
|
||||
AttributeOneWayPassword::class,
|
||||
AttributeEncryptedString::class,
|
||||
AttributePassword::class,
|
||||
];
|
||||
foreach ($aForbidenAttType as $sForbidenAttType) {
|
||||
if (is_a($sAttType, $sForbidenAttType, true)) {
|
||||
@@ -468,7 +482,7 @@ EOF
|
||||
if (!in_array($sLabel, $aGroupBy)) {
|
||||
$aGroupBy[$sAttCode] = $sLabel;
|
||||
|
||||
if (is_a($sAttType, 'AttributeDateTime', true)) {
|
||||
if (is_a($sAttType, AttributeDateTime::class, true)) {
|
||||
$aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel);
|
||||
$aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel);
|
||||
$aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel);
|
||||
@@ -1102,7 +1116,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
$aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass);
|
||||
if (isset($aAttributeTypes[$this->sGroupByAttCode])) {
|
||||
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
|
||||
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
|
||||
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
|
||||
$this->sOrderDirection = 'asc';
|
||||
} else {
|
||||
$this->sOrderDirection = 'desc';
|
||||
@@ -1258,7 +1272,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
if ($this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) {
|
||||
$aAttributeTypes = $this->oModelReflection->ListAttributes($sClass);
|
||||
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
|
||||
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
|
||||
if (is_subclass_of($sAttributeType, AttributeDateTime::class) || $sAttributeType == AttributeDateTime::class) {
|
||||
// Note: an alternative to this somewhat hardcoded way of doing things would be to implement...
|
||||
//$oExpr = Expression::FromOQL($this->sGroupByExpr);
|
||||
//$aTranslationData = array($oQuery->GetClassAlias() => array($this->sGroupByAttCode => new ScalarExpression(date('Y-m-d H:i:s', $iTime))));
|
||||
@@ -1289,7 +1303,7 @@ abstract class DashletGroupBy extends Dashlet
|
||||
foreach ($aValues as $sValue) {
|
||||
$aDisplayValues[] = ['label' => $sValue, 'value' => (int)rand(1, 15)];
|
||||
}
|
||||
} elseif (is_subclass_of($sAttributeType, 'AttributeEnum') || $sAttributeType == 'AttributeEnum') {
|
||||
} elseif (is_subclass_of($sAttributeType, AttributeEnum::class) || $sAttributeType == AttributeEnum::class) {
|
||||
$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $this->sGroupByAttCode);
|
||||
if ($aAllowed) { // null for non enums
|
||||
foreach ($aAllowed as $sValue => $sValueLabel) {
|
||||
@@ -1458,11 +1472,11 @@ abstract class DashletGroupBy extends Dashlet
|
||||
}
|
||||
foreach ($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
switch ($sAttType) {
|
||||
case 'AttributeDecimal':
|
||||
case 'AttributeDuration':
|
||||
case 'AttributeInteger':
|
||||
case 'AttributePercentage':
|
||||
case 'AttributeSubItem': // TODO: Known limitation: no unit displayed (values in sec)
|
||||
case AttributeDecimal::class:
|
||||
case AttributeDuration::class:
|
||||
case AttributeInteger::class:
|
||||
case AttributePercentage::class:
|
||||
case AttributeSubItem::class: // TODO: Known limitation: no unit displayed (values in sec)
|
||||
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aFunctionAttributes[$sAttCode] = $sLabel;
|
||||
break;
|
||||
|
||||
@@ -1078,7 +1078,6 @@ JS
|
||||
|
||||
if ($aCounts[$sStateValue] == 0) {
|
||||
$aCounts[$sStateValue] = ['link' => '-', 'label' => $aCounts[$sStateValue]];
|
||||
;
|
||||
} else {
|
||||
$oSingleGroupByValueFilter = $this->m_oFilter->DeepClone();
|
||||
$oSingleGroupByValueFilter->AddCondition($sStateAttrCode, $sStateValue, '=');
|
||||
@@ -1708,7 +1707,7 @@ JS
|
||||
$oBlock->bAdvancedMode = utils::ReadParam('advanced', false);
|
||||
|
||||
$oBlock->sCsvFile = strtolower($this->m_oFilter->GetClass()).'.csv';
|
||||
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
|
||||
$oBlock->sDownloadLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?expression='.urlencode($this->m_oFilter->ToOQL(true)).'&format=csv&filename='.urlencode($oBlock->sCsvFile);
|
||||
$oBlock->sLinkToToggle = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search'.$oAppContext->GetForLink(true).'&filter='.rawurlencode($this->m_oFilter->serialize()).'&format=csv';
|
||||
// Pass the parameters via POST, since expression may be very long
|
||||
$aParamsToPost = [
|
||||
@@ -1725,7 +1724,7 @@ JS
|
||||
$oBlock->sLinkToToggle = $oBlock->sLinkToToggle.'&advanced=1';
|
||||
$oBlock->sChecked = '';
|
||||
}
|
||||
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export.php';
|
||||
$oBlock->sAjaxLink = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php';
|
||||
|
||||
$oBlock->sCharsetNotice = false;
|
||||
$oBlock->sJsonParams = json_encode($aParamsToPost);
|
||||
@@ -1947,8 +1946,8 @@ class MenuBlock extends DisplayBlock
|
||||
$sSelectedClassName = MetaModel::GetName($sSelectedClass);
|
||||
|
||||
// Check rights on class
|
||||
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY) && ($oReflectionClass->IsSubclassOf('cmdbAbstractObject'));
|
||||
$bIsBulkDeleteAllowed = (bool) UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE);
|
||||
$bIsBulkModifyAllowed = (!MetaModel::IsAbstract($sSelectedClass)) && UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_MODIFY) && (($oReflectionClass->IsSubclassOf(cmdbAbstractObject::class) || $sSelectedClass === SynchroReplica::class));
|
||||
$bIsBulkDeleteAllowed = (bool)UserRights::IsActionAllowed($sSelectedClass, UR_ACTION_BULK_DELETE);
|
||||
|
||||
// Refine filter on selected class so bullk actions occur on the right class
|
||||
$oSelectedClassFilter = $this->GetFilter()->DeepClone();
|
||||
@@ -1958,7 +1957,15 @@ class MenuBlock extends DisplayBlock
|
||||
// Action label dict code has a specific suffix for "Link" / "Remote" aliases to allow dedicated labels in linksets.
|
||||
$sActionLabelCodeSuffix = in_array($sSelectedAlias, ['Link', 'Remote']) ? $sSelectedAlias : 'Class';
|
||||
if ($bIsBulkModifyAllowed) {
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:ModifyAll:'.$sSelectedAlias, Dict::Format('UI:Menu:ModifyAll_'.$sActionLabelCodeSuffix, $sSelectedClassName));
|
||||
if ($sSelectedClass === SynchroReplica::class) {
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:UnlinkAll:', Dict::S('Class:SynchroReplica/Action:unlink_all'), 'unlink');
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:UnLinkSynchroAll:', Dict::S('Class:SynchroReplica/Action:unlinksynchro_all'), 'unlinksynchro');
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:SynchroAll:', Dict::S('Class:SynchroReplica/Action:synchro_all'), 'synchro');
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:AllowDeleteAll:', Dict::S('Class:SynchroReplica/Action:allowdelete_all'), 'allowdelete');
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:DenyDeleteAll:', Dict::S('Class:SynchroReplica/Action:denydelete_all'), 'denydelete');
|
||||
} else {
|
||||
$this->AddBulkModifyObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:ModifyAll:'.$sSelectedAlias, Dict::Format('UI:Menu:ModifyAll_'.$sActionLabelCodeSuffix, $sSelectedClassName));
|
||||
}
|
||||
}
|
||||
if ($bIsBulkDeleteAllowed) {
|
||||
$this->AddBulkDeleteObjectsMenuAction($aRegularActions, $sSelectedClass, $oSelectedClassFilter->serialize(), 'UI:Menu:BulkDelete:'.$sSelectedAlias, Dict::Format('UI:Menu:BulkDelete_'.$sActionLabelCodeSuffix, $sSelectedClassName));
|
||||
@@ -2478,11 +2485,11 @@ class MenuBlock extends DisplayBlock
|
||||
* @since 3.1.0
|
||||
* @internal
|
||||
*/
|
||||
protected function AddBulkModifyObjectsMenuAction(array &$aActions, string $sClass, string $sFilter, string $sActionIdentifier = 'UI:Menu:ModifyAll', $sActionLabel = 'UI:Menu:ModifyAll'): void
|
||||
protected function AddBulkModifyObjectsMenuAction(array &$aActions, string $sClass, string $sFilter, string $sActionIdentifier = 'UI:Menu:ModifyAll', $sActionLabel = 'UI:Menu:ModifyAll', $sOperationName = 'modify'): void
|
||||
{
|
||||
$aActions[$sActionIdentifier] = [
|
||||
'label' => Dict::S($sActionLabel),
|
||||
'url' => $this->PrepareUrlForStandardMenuAction($sClass, "operation=select_for_modify_all&class=$sClass&filter=".urlencode($sFilter)),
|
||||
'label' => Dict::S($sActionLabel),
|
||||
'url' => $this->PrepareUrlForStandardMenuAction($sClass, 'operation=select_for_'.$sOperationName.'_all&class='.$sClass.'&filter='.urlencode($sFilter)),
|
||||
] + $this->GetDefaultParamsForMenuAction();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use Combodo\iTop\Application\Helper\Session;
|
||||
/**
|
||||
* Class LoginBasic
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
@@ -24,11 +24,7 @@ class LoginBasic extends AbstractLoginFSMExtension
|
||||
protected function OnModeDetection(&$iErrorCode)
|
||||
{
|
||||
if (!Session::IsSet('login_mode')) {
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
Session::Set('login_mode', 'basic');
|
||||
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
|
||||
Session::Set('login_mode', 'basic');
|
||||
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
if (!is_null($this->GetAuthUserAndPassword())) {
|
||||
Session::Set('login_mode', 'basic');
|
||||
}
|
||||
}
|
||||
@@ -86,38 +82,49 @@ class LoginBasic extends AbstractLoginFSMExtension
|
||||
return LoginWebPage::LOGIN_FSM_CONTINUE;
|
||||
}
|
||||
|
||||
private function GetAuthUserAndPassword()
|
||||
/**
|
||||
* @return array|null [$sAuthUser, $sAuthPwd]
|
||||
*/
|
||||
private function GetAuthUserAndPassword(): ?array
|
||||
{
|
||||
$sAuthUser = '';
|
||||
$sAuthPwd = null;
|
||||
$sAuthorization = '';
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
$sAuthorization = $_SERVER['HTTP_AUTHORIZATION'];
|
||||
} elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
|
||||
$sAuthorization = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
|
||||
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
|
||||
// Unfortunately, the RFC is not clear about the encoding...
|
||||
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
|
||||
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
|
||||
if (!LoginWebPage::LooksLikeUTF8($sAuthUser)) {
|
||||
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
|
||||
// Supposed to be harmless in case of a plain ASCII string...
|
||||
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
|
||||
}
|
||||
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
|
||||
if (!LoginWebPage::LooksLikeUTF8($sAuthPwd)) {
|
||||
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
|
||||
// Supposed to be harmless in case of a plain ASCII string...
|
||||
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
|
||||
}
|
||||
|
||||
return [$sAuthUser, $sAuthPwd];
|
||||
}
|
||||
|
||||
if (!empty($sAuthorization)) {
|
||||
list($sAuthUser, $sAuthPwd) = explode(':', base64_decode(substr($sAuthorization, 6)));
|
||||
} else {
|
||||
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
|
||||
// Unfortunately, the RFC is not clear about the encoding...
|
||||
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
|
||||
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
|
||||
if (!LoginWebPage::LooksLikeUTF8($sAuthUser)) {
|
||||
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
|
||||
// Supposed to be harmless in case of a plain ASCII string...
|
||||
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
|
||||
}
|
||||
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
|
||||
if (!LoginWebPage::LooksLikeUTF8($sAuthPwd)) {
|
||||
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
|
||||
// Supposed to be harmless in case of a plain ASCII string...
|
||||
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
|
||||
}
|
||||
}
|
||||
if (empty($sAuthorization)) {
|
||||
return null;
|
||||
}
|
||||
return [$sAuthUser, $sAuthPwd];
|
||||
|
||||
// Decode without Bearer
|
||||
if (!utils::StartsWith($sAuthorization, 'Basic ')) {
|
||||
return null;
|
||||
}
|
||||
$sDecodedAuthorisation = base64_decode(substr($sAuthorization, 6));
|
||||
if (str_contains($sDecodedAuthorisation, ':')) {
|
||||
return explode(':', $sDecodedAuthorisation);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Branding;
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\NiceWebPage;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Service\Session\Session;
|
||||
|
||||
/**
|
||||
* Web page used for displaying the login form
|
||||
@@ -369,16 +369,7 @@ class LoginWebPage extends NiceWebPage
|
||||
public static function ResetSession()
|
||||
{
|
||||
// Unset all of the session variables.
|
||||
Session::Unset('auth_user');
|
||||
Session::Unset('login_state');
|
||||
Session::Unset('can_logoff');
|
||||
Session::Unset('archive_mode');
|
||||
Session::Unset('impersonate_user');
|
||||
Session::Unset('PluginProperties');
|
||||
Session::Unset('UrlMakerClass');
|
||||
Session::Unset('itop_env');
|
||||
Session::Unset('obj_messages');
|
||||
Session::Unset('profile_list');
|
||||
Session::UnsetAll();
|
||||
UserRights::_ResetSessionCache();
|
||||
// If it's desired to kill the session, also delete the session cookie.
|
||||
// Note: This will destroy the session, and not just the session data!
|
||||
|
||||
@@ -113,17 +113,43 @@ abstract class Query extends cmdbAbstractObject
|
||||
]
|
||||
));
|
||||
|
||||
MetaModel::Init_AddAttribute(new AttributeExternalKey(
|
||||
"owner_id",
|
||||
[
|
||||
"targetclass" => 'Contact',
|
||||
"allowed_values" => null,
|
||||
"sql" => 'owner_id',
|
||||
"is_null_allowed" => true,
|
||||
"depends_on" => [],
|
||||
"display_style" => 'select',
|
||||
"always_load_in_tables" => false,
|
||||
"on_target_delete" => DEL_AUTO,
|
||||
]
|
||||
));
|
||||
|
||||
MetaModel::Init_AddAttribute(new AttributeEnumSet(
|
||||
"usages",
|
||||
[
|
||||
"allowed_values" => null,
|
||||
"possible_values" => new ValueSetEnumPadded("export,reference,notif,draft,dashlet"),
|
||||
"sql" => "usages",
|
||||
"depends_on" => [],
|
||||
"is_null_allowed" => true,
|
||||
"max_items" => 12,
|
||||
]
|
||||
));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems(
|
||||
'details',
|
||||
['name', 'is_template', 'description']
|
||||
['name', 'is_template', 'owner_id', 'usages', 'description']
|
||||
); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', ['description']); // Attributes to be displayed for a list
|
||||
MetaModel::Init_SetZListItems('list', ['description', 'usages']); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', ['name', 'description', 'is_template']); // Criteria of the std search form
|
||||
MetaModel::Init_SetZListItems(
|
||||
'default_search',
|
||||
['name', 'description', 'is_template']
|
||||
['name', 'description', 'is_template', 'owner_id', 'usages']
|
||||
); // Criteria of the default search form
|
||||
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
|
||||
}
|
||||
@@ -143,6 +169,15 @@ abstract class Query extends cmdbAbstractObject
|
||||
return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
|
||||
}
|
||||
|
||||
public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = [])
|
||||
{
|
||||
// read only attribute
|
||||
if (in_array($sAttCode, ['export_count', 'export_last_date', 'export_last_user_id'])) {
|
||||
return OPT_ATT_READONLY;
|
||||
}
|
||||
return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return export url.
|
||||
*
|
||||
@@ -214,15 +249,15 @@ class QueryOQL extends Query
|
||||
MetaModel::Init_SetZListItems(
|
||||
'details',
|
||||
[
|
||||
'col:col1' => ['fieldset:Query:baseinfo' => ['name', 'is_template', 'description', 'oql', 'fields']],
|
||||
'col:col1' => ['fieldset:Query:baseinfo' => ['name', 'is_template', 'owner_id', 'usages', 'description', 'oql', 'fields']],
|
||||
'col:col2' => ['fieldset:Query:exportInfo' => ['export_count', 'export_last_date', 'export_last_user_id', 'export_last_user_contact']],
|
||||
]
|
||||
); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', ['description']); // Attributes to be displayed for a list
|
||||
MetaModel::Init_SetZListItems('list', ['description', 'usages']); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems(
|
||||
'standard_search',
|
||||
['name', 'description', 'is_template', 'fields', 'oql']
|
||||
['name', 'description', 'is_template', 'owner_id', 'usages', 'oql']
|
||||
); // Criteria of the std search form
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Service\Startup\StartupService;
|
||||
|
||||
require_once(APPROOT.'core/cmdbobject.class.inc.php');
|
||||
require_once(APPROOT.'application/utils.inc.php');
|
||||
@@ -69,26 +70,7 @@ $oKPI->ComputeAndReport("Session Start");
|
||||
|
||||
$sSwitchEnv = utils::ReadParam('switch_env', null);
|
||||
$bAllowCache = true;
|
||||
if (($sSwitchEnv != null) && file_exists(APPCONF.$sSwitchEnv.'/'.ITOP_CONFIG_FILE) && (Session::Get('itop_env') !== $sSwitchEnv)) {
|
||||
Session::Set('itop_env', $sSwitchEnv);
|
||||
$sEnv = $sSwitchEnv;
|
||||
$bAllowCache = false;
|
||||
// Reset the opcache since otherwise the PHP "model" files may still be cached !!
|
||||
if (function_exists('opcache_reset')) {
|
||||
// Zend opcode cache
|
||||
opcache_reset();
|
||||
}
|
||||
if (function_exists('apc_clear_cache')) {
|
||||
// APC(u) cache
|
||||
apc_clear_cache();
|
||||
}
|
||||
// TODO: reset the credentials as well ??
|
||||
} elseif (Session::IsSet('itop_env')) {
|
||||
$sEnv = Session::Get('itop_env');
|
||||
} else {
|
||||
$sEnv = ITOP_DEFAULT_ENV;
|
||||
Session::Set('itop_env', ITOP_DEFAULT_ENV);
|
||||
}
|
||||
$sEnv = (new StartupService())->SetItopEnvironment($sSwitchEnv, $bAllowCache);
|
||||
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
|
||||
try {
|
||||
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
|
||||
|
||||
@@ -1886,7 +1886,7 @@ SQL;
|
||||
CURLOPT_HEADER => false, // don't return the headers in the output
|
||||
CURLOPT_FOLLOWLOCATION => true, // follow redirects
|
||||
CURLOPT_ENCODING => "", // handle all encodings
|
||||
CURLOPT_USERAGENT => "spider", // who am i
|
||||
CURLOPT_USERAGENT => static::GetConfig()->Get('http.request.user_agent'), // who am i
|
||||
CURLOPT_AUTOREFERER => true, // set referer on redirect
|
||||
CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect
|
||||
CURLOPT_TIMEOUT => 120, // timeout on response
|
||||
@@ -3022,7 +3022,6 @@ TXT
|
||||
* Note: Only works for backoffice URLs for now
|
||||
*
|
||||
* @param string $sText Text containing the mentioned objects to be found
|
||||
* @param string $sFormat {@uses static::ENUM_TEXT_FORMAT_HTML, ...}
|
||||
*
|
||||
* @return array Array of object classes / IDs for the ones found in $sText
|
||||
*
|
||||
@@ -3037,13 +3036,45 @@ TXT
|
||||
public static function GetMentionedObjectsFromText(string $sText): array
|
||||
{
|
||||
$aMentionedObjects = [];
|
||||
$aMentionMatches = [];
|
||||
$sText = html_entity_decode($sText);
|
||||
$aMentionAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
|
||||
$oDom = new \DOMDocument();
|
||||
$bPreviousUseInternalErrors = libxml_use_internal_errors(true); // to keep processing even in case of "invalid" HTML, cf. testGetMentionedObjectsFromText
|
||||
|
||||
preg_match_all('/<a\s*([^>]*)data-object-class="([^"]*)"\s.*data-object-key="([^"]*)"/Ui', $sText, $aMentionMatches);
|
||||
foreach ($aMentionMatches[0] as $iMatchIdx => $sCompleteMatch) {
|
||||
$sMatchedClass = $aMentionMatches[2][$iMatchIdx];
|
||||
$sMatchedId = $aMentionMatches[3][$iMatchIdx];
|
||||
try {
|
||||
$oDom->loadHTML('<?xml encoding="UTF-8">'.$sText);
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($bPreviousUseInternalErrors);
|
||||
}
|
||||
$oXpath = new \DOMXPath($oDom);
|
||||
$oNodes = $oXpath->query('//a[@data-object-class and @data-object-key]');
|
||||
|
||||
foreach ($oNodes as $oObjNode) {
|
||||
$sMatchedClass = $oObjNode->getAttribute('data-object-class');
|
||||
$sMatchedId = $oObjNode->getAttribute('data-object-key');
|
||||
$sMatchedName = trim($oObjNode->textContent);
|
||||
|
||||
// Ensure that what we found is actually configured as a mention
|
||||
$sMentionPrefix = array_search($sMatchedClass, $aMentionAllowedClasses);
|
||||
// - No direct configuration for the matched class, let's look for a configuration on parent classes
|
||||
if (false === $sMentionPrefix) {
|
||||
$bHasMentionConfigurationForParentClass = false;
|
||||
foreach (MetaModel::EnumParentClasses($sMatchedClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false) as $aMatchedParentClass) {
|
||||
$sMentionPrefix = array_search($aMatchedParentClass, $aMentionAllowedClasses);
|
||||
if (false !== $sMentionPrefix) {
|
||||
$bHasMentionConfigurationForParentClass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $bHasMentionConfigurationForParentClass) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// - Test if the name starts with $sMentionPrefix (e.g. '@' for 'Contact' class)
|
||||
if (false === str_starts_with($sMatchedName, $sMentionPrefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare array for matched class if not already present
|
||||
if (!array_key_exists($sMatchedClass, $aMentionedObjects)) {
|
||||
@@ -3218,4 +3249,48 @@ TXT
|
||||
return (int)$sLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0 N°9875
|
||||
* Open archive and raise appropriate exception.
|
||||
* Warning: do not forget to close archive afterwhile
|
||||
* @param string $sArchiveFilePath
|
||||
* @param int|null $flags
|
||||
* @return ZipArchive
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function ZipArchiveOpen(string $sArchiveFilePath, int|null $flags = null): ZipArchive
|
||||
{
|
||||
$oZip = new ZipArchive();
|
||||
if (is_null($flags)) {
|
||||
$code = $oZip->open($sArchiveFilePath);
|
||||
} else {
|
||||
$code = $oZip->open($sArchiveFilePath, $flags);
|
||||
}
|
||||
if (true !== $code) {
|
||||
//ZipArchive::ZIP_ER_NOZIP : 19
|
||||
if ($code === 19) {
|
||||
throw new \Exception(sprintf('Cannot open zip file due to inconsistent or empty content'));
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Cannot open zip file due to error code %s', $code));
|
||||
}
|
||||
return $oZip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.3.0 N°9875
|
||||
* @param string $sDirectory
|
||||
* @param string $sPrefix
|
||||
* @return ZipArchive
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function ZipArchiveCreateWithTempNam(string $sDirectory, string $sPrefix): ZipArchive
|
||||
{
|
||||
$sTempnam = tempnam($sDirectory, $sPrefix);
|
||||
unlink($sTempnam);
|
||||
$sArchiveName = $sTempnam.'.zip';
|
||||
|
||||
return self::ZipArchiveOpen($sArchiveName, ZipArchive::CREATE);
|
||||
}
|
||||
}
|
||||
|
||||
14
composer.lock
generated
14
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1cca58a0e26794fb9c546a001f7f7de8",
|
||||
"content-hash": "24e7507794b971955358df5f2b143ac4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "apereo/phpcas",
|
||||
@@ -5570,16 +5570,16 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.24.0",
|
||||
"version": "v3.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "a6769aefb305efef849dc25c9fd1653358c148f0"
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a6769aefb305efef849dc25c9fd1653358c148f0",
|
||||
"reference": "a6769aefb305efef849dc25c9fd1653358c148f0",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5634,7 +5634,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.24.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5646,7 +5646,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-17T21:31:11+00:00"
|
||||
"time": "2026-07-03T20:44:34+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
||||
@@ -916,7 +916,9 @@ class CMDBSource
|
||||
$aColumn = [];
|
||||
$aData = self::QueryToArray($sSql);
|
||||
foreach ($aData as $aRow) {
|
||||
@$aColumn[] = $aRow[$col];
|
||||
if ($aRow[$col] !== null) {
|
||||
$aColumn[] = $aRow[$col];
|
||||
}
|
||||
}
|
||||
return $aColumn;
|
||||
}
|
||||
|
||||
@@ -730,13 +730,8 @@ abstract class DBObject implements iDisplay
|
||||
public function SetTrim($sAttCode, $sValue)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
|
||||
$iMaxSize = $oAttDef->GetMaxSize();
|
||||
$sLength = mb_strlen($sValue);
|
||||
if ($iMaxSize && ($sLength > $iMaxSize)) {
|
||||
$sMessage = " -truncated ($sLength chars)";
|
||||
$sValue = mb_substr($sValue, 0, $iMaxSize - mb_strlen($sMessage)).$sMessage;
|
||||
}
|
||||
$this->Set($sAttCode, $sValue);
|
||||
|
||||
$this->Set($sAttCode, $oAttDef->TrimValue($sValue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2038,7 +2033,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
if (!is_null($iMaxSize = $oAtt->GetMaxSize())) {
|
||||
$iLen = mb_strlen($toCheck);
|
||||
$iLen = $oAtt->GetSize($toCheck);
|
||||
if ($iLen > $iMaxSize) {
|
||||
return "String too long (found $iLen, limited to $iMaxSize)";
|
||||
}
|
||||
|
||||
@@ -1935,9 +1935,8 @@ class DBObjectSearch extends DBSearch
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DBObjectSearch
|
||||
*/
|
||||
protected function ApplyDataFilters(): DBObjectSearch
|
||||
protected function ApplyDataFilters(): DBSearch
|
||||
{
|
||||
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
|
||||
return $this;
|
||||
|
||||
@@ -676,9 +676,8 @@ class DBUnionSearch extends DBSearch
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DBUnionSearch
|
||||
*/
|
||||
protected function ApplyDataFilters(): DBUnionSearch
|
||||
protected function ApplyDataFilters(): DBSearch
|
||||
{
|
||||
if ($this->IsAllDataAllowed() || $this->IsDataFiltered()) {
|
||||
return $this;
|
||||
|
||||
@@ -434,7 +434,7 @@ JS
|
||||
* Resize an image so that it fits the maximum width/height defined in the config file
|
||||
* @param ormDocument $oImage The original image stored as an array (content / mimetype / filename)
|
||||
* @return ormDocument The resampled image (or the original one if it already fit)
|
||||
* @deprecated since 3.3.0 Replaced by ormDocument::ResizeImageToFit
|
||||
* @deprecated 3.3.0 Use ormDocument::ResizeImageToFit instead
|
||||
*/
|
||||
public static function ResizeImageToFit(ormDocument $oImage, &$aDimensions = null)
|
||||
{
|
||||
|
||||
@@ -614,6 +614,13 @@ class LogChannels
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public const SECURITY = 'Security';
|
||||
|
||||
/**
|
||||
* For Session parameters
|
||||
*
|
||||
* @Since 3.3.0
|
||||
*/
|
||||
public const SESSION_PARAMETERS = 'SessionParameters';
|
||||
}
|
||||
|
||||
abstract class LogAPI
|
||||
|
||||
@@ -22,7 +22,6 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Setup\ModuleDependency\Module;
|
||||
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
|
||||
|
||||
require_once APPROOT.'core/modulehandler.class.inc.php';
|
||||
@@ -1643,10 +1642,10 @@ abstract class MetaModel
|
||||
if ($sFilterCode == 'id') {
|
||||
return true;
|
||||
}
|
||||
if (array_key_exists($sClass, self::$m_aMagicFields) && array_key_exists($sFilterCode, self::$m_aMagicFields[$sClass])) {
|
||||
if (array_key_exists($sClass, self::$m_aMagicFields) && in_array($sFilterCode, self::$m_aMagicFields[$sClass])) {
|
||||
return true;
|
||||
}
|
||||
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && array_key_exists($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
|
||||
if (array_key_exists($sClass, self::$m_aFilterForbiddenAttributes) && in_array($sFilterCode, self::$m_aFilterForbiddenAttributes[$sClass])) {
|
||||
return false;
|
||||
}
|
||||
return self::IsValidAttCode($sClass, $sFilterCode);
|
||||
@@ -2964,8 +2963,9 @@ abstract class MetaModel
|
||||
self::$m_aMagicFields[] = $sClass;
|
||||
}
|
||||
self::$m_aMagicFields[$sClass][] = $sCode;
|
||||
self::$m_aAttribOrigins[$sClass][$sCode] = self::$m_aAttribOrigins[$sClass][$sAttCode];
|
||||
}
|
||||
if (!$oAttDef->IsSearchable()) {
|
||||
if (!$oAttDef->IsValidFilterAttribute()) {
|
||||
if (!array_key_exists($sClass, self::$m_aFilterForbiddenAttributes)) {
|
||||
self::$m_aFilterForbiddenAttributes[] = $sClass;
|
||||
}
|
||||
@@ -4660,12 +4660,15 @@ abstract class MetaModel
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public static function DBExists($bMustBeComplete = true)
|
||||
public static function DBExists($bMustBeComplete = true, ?string $sDBName = null)
|
||||
{
|
||||
if (!CMDBSource::IsDB(self::$m_sDBName)) {
|
||||
if (is_null($sDBName)) {
|
||||
$sDBName = self::$m_sDBName;
|
||||
}
|
||||
if (!CMDBSource::IsDB($sDBName)) {
|
||||
return false;
|
||||
}
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
CMDBSource::SelectDB($sDBName);
|
||||
|
||||
$aFound = [];
|
||||
$aMissing = [];
|
||||
@@ -5759,36 +5762,37 @@ abstract class MetaModel
|
||||
|
||||
self::$m_sEnvironment = $sEnvironment;
|
||||
|
||||
try {
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
if (!defined('MODULESROOT')) {
|
||||
define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/');
|
||||
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
self::$m_bTraceSourceFiles = $bTraceSourceFiles;
|
||||
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
if ($bModelOnly) {
|
||||
return;
|
||||
}
|
||||
// $config can be either a filename, or a Configuration object (volatile!)
|
||||
if ($config instanceof Config) {
|
||||
self::LoadConfig($config, $bAllowCache);
|
||||
} else {
|
||||
self::LoadConfig(new Config($config), $bAllowCache);
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
if ($bModelOnly) {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
return;
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
} finally {
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
CMDBSource::SelectDB(self::$m_sDBName);
|
||||
|
||||
foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) {
|
||||
$oPHPClass::OnMetaModelStarted();
|
||||
}
|
||||
|
||||
ExpressionCache::Warmup();
|
||||
|
||||
// Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface
|
||||
EventService::InitService();
|
||||
EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,15 +60,13 @@ class OQLActualClassTreeResolver
|
||||
$aTranslateFields = [];
|
||||
foreach ($aExpectedAttributes as $sAttCode => $oExpression) {
|
||||
// 'id' is managed later
|
||||
if ($sAttCode == 'id') {
|
||||
if ($sAttCode === 'id') {
|
||||
continue;
|
||||
}
|
||||
// Attributes can be stored in attributes list or for magic ones into filter codes list.
|
||||
$sOriginClass = null;
|
||||
if (MetaModel::IsValidAttCode($sClass, $sAttCode)) {
|
||||
if (MetaModel::IsValidAttCode($sClass, $sAttCode) || MetaModel::IsValidFilterCode($sClass, $sAttCode)) {
|
||||
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sAttCode);
|
||||
} elseif ($sAttCode == 'id') {
|
||||
$sOriginClass = $sClass;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ class ormDocument
|
||||
* @param int $iMaxImageHeight Maximum height for the resized image
|
||||
* @param array|null $aFinalDimensions Image dimensions after resizing or null if unable to read the image
|
||||
* @return ormDocument The resampled image
|
||||
*
|
||||
* @since 3.3.0 N°3124
|
||||
*/
|
||||
public function ResizeImageToFit(int $iMaxWidth, int $iMaxHeight, array|null &$aFinalDimensions = null): static
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ class ormPassword
|
||||
public function SetPassword($sClearTextPassword)
|
||||
{
|
||||
$iHashAlgo = MetaModel::GetConfig()->GetPasswordHashAlgo();
|
||||
$this->m_sHashed = password_hash($sClearTextPassword, $iHashAlgo);
|
||||
$this->m_sHashed = password_hash($sClearTextPassword ?? '', $iHashAlgo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,10 +99,10 @@ class ormPassword
|
||||
$aInfo = password_get_info($this->m_sHashed);
|
||||
if (is_null($aInfo["algo"]) || $aInfo["algo"] === 0) {
|
||||
// - Unknown algorithm, assume it's a legacy password
|
||||
$sHashedPwd = $this->ComputeHash($sClearTextPassword);
|
||||
$sHashedPwd = $this->ComputeHash($sClearTextPassword ?? '');
|
||||
$bResult = hash_equals($this->m_sHashed, $sHashedPwd);
|
||||
} else {
|
||||
$bResult = password_verify($sClearTextPassword, $this->m_sHashed);
|
||||
$bResult = password_verify($sClearTextPassword ?? '', $this->m_sHashed);
|
||||
}
|
||||
return $bResult;
|
||||
}
|
||||
|
||||
@@ -248,6 +248,45 @@ class RestResultWithObjects extends RestResult
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @package RESTAPI
|
||||
* @api
|
||||
*/
|
||||
class RestResultWithObjectSets extends RestResultWithObjects
|
||||
{
|
||||
private $current_object = null;
|
||||
|
||||
public function MakeNewObjectSet()
|
||||
{
|
||||
$arr = [];
|
||||
$this->current_object = &$arr;
|
||||
$this->objects[] = &$arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the given object
|
||||
*
|
||||
* @api
|
||||
* @param string $sObjectAlias Name of the subobject, usually the OQL class alias
|
||||
* @param int $iCode An error code (RestResult::OK is no issue has been found)
|
||||
* @param string $sMessage Description of the error if any, an empty string otherwise
|
||||
* @param DBObject $oObject The object being reported
|
||||
* @param array|null $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
|
||||
* @param boolean $bExtendedOutput Output all of the link set attributes ?
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function AppendSubObject($sObjectAlias, $iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
|
||||
{
|
||||
$oObjRes = ObjectResult::FromDBObject($oObject, $aFieldSpec, $bExtendedOutput, $iCode, $sMessage);
|
||||
$this->current_object[$sObjectAlias] = $oObjRes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @package RESTAPI
|
||||
* @api
|
||||
@@ -500,14 +539,21 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
|
||||
break;
|
||||
|
||||
case 'core/get':
|
||||
$sClass = RestUtils::GetClass($aParams, 'class');
|
||||
$sClassParam = RestUtils::GetMandatoryParam($aParams, 'class');
|
||||
$key = RestUtils::GetMandatoryParam($aParams, 'key');
|
||||
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
|
||||
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
|
||||
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
|
||||
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
|
||||
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
|
||||
|
||||
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
|
||||
// Validate the class(es)
|
||||
$aClass = explode(',', $sClassParam);
|
||||
foreach ($aClass as $sClass) {
|
||||
if (!MetaModel::IsValidClass(trim($sClass))) {
|
||||
throw new Exception("class '$sClass' is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
$oObjectSet = RestUtils::GetObjectSetFromKey($sClassParam, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
|
||||
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
|
||||
|
||||
if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) {
|
||||
@@ -519,8 +565,56 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
|
||||
} elseif ($iPage < 1) {
|
||||
$oResult->code = RestResult::INVALID_PAGE;
|
||||
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
|
||||
} elseif (count($oObjectSet->GetSelectedClasses()) > 1) {
|
||||
$oResult = new RestResultWithObjectSets();
|
||||
$aCache = [];
|
||||
$aShowFields = [];
|
||||
foreach ($oObjectSet->GetSelectedClasses() as $sSelectedClass) {
|
||||
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
|
||||
}
|
||||
|
||||
while ($oObjects = $oObjectSet->FetchAssoc()) {
|
||||
$oResult->MakeNewObjectSet();
|
||||
|
||||
foreach ($oObjects as $sAlias => $oObject) {
|
||||
if (!$oObject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists($sAlias, $aCache)) {
|
||||
$sClass = get_class($oObject);
|
||||
$bExtendedOutput = RestUtils::HasRequestedExtendedOutput($sShowFields);
|
||||
|
||||
if (!RestUtils::HasRequestedAllOutputFields($sShowFields)) {
|
||||
$aFields = $aShowFields[$sClass];
|
||||
//Id is not a valid attribute to optimize
|
||||
if ($aFields && in_array('id', $aFields)) {
|
||||
unset($aFields[array_search('id', $aFields)]);
|
||||
}
|
||||
$aAttToLoad = [$sAlias => $aFields];
|
||||
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
|
||||
}
|
||||
$aCache[$sAlias] = [
|
||||
'aShowFields' => $aShowFields,
|
||||
'bExtendedOutput' => $bExtendedOutput,
|
||||
];
|
||||
} else {
|
||||
$aShowFields = $aCache[$sAlias]['aShowFields'];
|
||||
$bExtendedOutput = $aCache[$sAlias]['bExtendedOutput'];
|
||||
}
|
||||
|
||||
$oResult->AppendSubObject($sAlias, 0, '', $oObject, $aShowFields, $bExtendedOutput);
|
||||
}
|
||||
}
|
||||
$oResult->message = "Found: ".$oObjectSet->Count();
|
||||
} else {
|
||||
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*') {
|
||||
$aShowFields = [];
|
||||
foreach ($aClass as $sSelectedClass) {
|
||||
$sSelectedClass = trim($sSelectedClass);
|
||||
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
|
||||
}
|
||||
|
||||
if (!RestUtils::HasRequestedAllOutputFields($sShowFields) && count($aShowFields) == 1) {
|
||||
$aFields = $aShowFields[$sClass];
|
||||
//Id is not a valid attribute to optimize
|
||||
if (in_array('id', $aFields)) {
|
||||
@@ -531,7 +625,7 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
|
||||
}
|
||||
|
||||
while ($oObject = $oObjectSet->Fetch()) {
|
||||
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
|
||||
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
|
||||
}
|
||||
$oResult->message = "Found: ".$oObjectSet->Count();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalField;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeExternalKey;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeFriendlyName;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeHierarchicalKey;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeLinkedSet;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeStopWatch;
|
||||
use Combodo\iTop\Core\AttributeDefinition\AttributeSubItem;
|
||||
|
||||
/**
|
||||
* Bulk export: Tabular export: abstract base class for all "tabular" exports.
|
||||
@@ -91,8 +98,8 @@ abstract class TabularBulkExport extends BulkExport
|
||||
{
|
||||
$aResult = [];
|
||||
switch (get_class($oAttDef)) {
|
||||
case 'AttributeExternalKey':
|
||||
case 'AttributeHierarchicalKey':
|
||||
case AttributeExternalKey::class:
|
||||
case AttributeHierarchicalKey::class:
|
||||
|
||||
$bAddFriendlyName = true;
|
||||
$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
@@ -142,7 +149,7 @@ abstract class TabularBulkExport extends BulkExport
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AttributeStopWatch':
|
||||
case AttributeStopWatch::class:
|
||||
foreach (MetaModel::ListAttributeDefs($sClass) as $sSubAttCode => $oSubAttDef) {
|
||||
if ($oSubAttDef instanceof AttributeSubItem) {
|
||||
if ($oSubAttDef->GetParentAttCode() == $sAttCode) {
|
||||
|
||||
@@ -1530,6 +1530,37 @@ class UserRights
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $oUser
|
||||
* @param array $aExcludedProfilesId Administrator by default, but can also be other proofiles depending on needs (e.g. power portal user or REST profile)
|
||||
* @return bool
|
||||
* @throws ArchivedObjectException
|
||||
* @throws CoreException
|
||||
* @throws CoreUnexpectedValue
|
||||
* @throws MySQLException
|
||||
*/
|
||||
public static function IsUserReadOnly(User $oUser, array $aExcludedProfilesId = [1]): bool
|
||||
{
|
||||
$oUserProfiles = $oUser->Get('profile_list');
|
||||
$oUserRights = UserRights::GetModuleInstance();
|
||||
while ($oUserProfile = $oUserProfiles->Fetch()) {
|
||||
$iProfileId = $oUserProfile->Get('profileid');
|
||||
if (in_array($iProfileId, $aExcludedProfilesId)) {
|
||||
return false;
|
||||
}
|
||||
foreach (MetaModel::GetClasses('bizmodel,grant_by_profile') as $sClass) {
|
||||
foreach (['w', 'bw', 'd', 'bd'] as $sWriteActionCode) {
|
||||
$bIsGranted = $oUserRights->GetProfileActionGrant($iProfileId, $sClass, $sWriteActionCode);
|
||||
if ($bIsGranted === true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
* @param int $iActionCode see UR_ACTION_* constants
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
$ibo-user-rights--padding-x: $ibo-spacing-400 !default;
|
||||
$ibo-user-rights--padding-y: $ibo-spacing-200 !default;
|
||||
$ibo-user-rights--padding-y: $ibo-spacing-100 !default;
|
||||
$ibo-user-rights--border-radius: $ibo-border-radius-400 !default;
|
||||
|
||||
$ibo-user-rights--is-success--background-color: $ibo-color-success-100 !default;
|
||||
@@ -18,6 +18,8 @@ $ibo-user-rights--is-failure--border-color: $ibo-color-danger-500 !default;
|
||||
$ibo-user-rights--is-failure--border: 1px solid $ibo-user-rights--is-failure--border-color !default;
|
||||
|
||||
.ibo-user-rights {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
padding: $ibo-user-rights--padding-y $ibo-user-rights--padding-x;
|
||||
border-radius: $ibo-user-rights--border-radius;
|
||||
&.ibo-is-success {
|
||||
|
||||
@@ -19,6 +19,8 @@ $ibo-badge-colors: (
|
||||
'orange' : ($ibo-color-orange-100, $ibo-color-orange-900),
|
||||
'red': ($ibo-color-red-100, $ibo-color-red-900),
|
||||
'pink': ($ibo-color-pink-100, $ibo-color-pink-900),
|
||||
'purple': ($ibo-color-purple-100, $ibo-color-purple-900),
|
||||
'yellow': ($ibo-color-yellow-200, $ibo-color-yellow-900),
|
||||
) !default;
|
||||
|
||||
|
||||
|
||||
@@ -8,70 +8,74 @@
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.help-text{
|
||||
padding: 1px 5px;
|
||||
background-color: #d7e3f8;
|
||||
border: 1px solid #c6e7f5;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.form-error ul{
|
||||
padding: 1px 5px;
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.subform{
|
||||
background-color: #efefef;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form-buttons{
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.form select{
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form select option{
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.turbo-refreshing{
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.ibo-field legend{
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
collection-entry-element {
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
padding: 10px 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ts-control{
|
||||
height: auto;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.ibo-form-actions > .ibo-button > span{
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ibo-form textarea{
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
// WIP SDK Forms
|
||||
//form[is="itop-form-element"] {
|
||||
//
|
||||
// .help-text{
|
||||
// padding: 1px 5px;
|
||||
// background-color: #d7e3f8;
|
||||
// border: 1px solid #c6e7f5;
|
||||
// border-radius: 5px;
|
||||
// margin: 5px 0;
|
||||
// font-size: 0.9em;
|
||||
// }
|
||||
//
|
||||
// .form-error ul{
|
||||
// padding: 1px 5px;
|
||||
// background-color: #f8d7da;
|
||||
// border: 1px solid #f5c6cb;
|
||||
// border-radius: 5px;
|
||||
// margin: 5px 0;
|
||||
// font-size: 0.9em;
|
||||
// }
|
||||
//
|
||||
// .subform{
|
||||
// background-color: #efefef;
|
||||
// border-radius: 5px;
|
||||
// padding: 10px;
|
||||
// }
|
||||
//
|
||||
// .form-buttons{
|
||||
// margin: 20px 0;
|
||||
// }
|
||||
//
|
||||
// .form select{
|
||||
// padding: 0;
|
||||
// overflow-y: auto;
|
||||
// }
|
||||
//
|
||||
// .form select option{
|
||||
// height: 30px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// }
|
||||
//
|
||||
// .turbo-refreshing{
|
||||
// opacity: .5;
|
||||
// }
|
||||
//
|
||||
// .ibo-field legend{
|
||||
// margin-top: 24px;
|
||||
// }
|
||||
//
|
||||
// collection-entry-element {
|
||||
// margin-top: 8px;
|
||||
// display: block;
|
||||
// padding: 10px 10px;
|
||||
// background-color: #f5f5f5;
|
||||
// border-radius: 5px;
|
||||
// }
|
||||
// .ts-control{
|
||||
// height: auto;
|
||||
// min-height: 30px;
|
||||
// }
|
||||
//
|
||||
// .ibo-form-actions > .ibo-button > span{
|
||||
// margin-right: 5px;
|
||||
// }
|
||||
//
|
||||
// .ibo-form textarea{
|
||||
// resize: vertical;
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
$ibo-input-select-icon--menu--icon--max-height: 100% !default;
|
||||
$ibo-input-select-icon--menu--icon--max-width: 100% !default;
|
||||
$ibo-input-select-icon--icon--padding-right: $ibo-spacing-200 !default;
|
||||
$ibo-input-select-icon--max-height: 100% !default;
|
||||
$ibo-input-select-icon--max-width: 100% !default;
|
||||
$ibo-input-select-icon--padding-right: $ibo-spacing-200 !default;
|
||||
|
||||
$ibo-input-select-icon--menu--z-index: 21 !default;
|
||||
$ibo-input-select-icon--menu--max-height: 300px !default;
|
||||
@@ -18,9 +18,9 @@ $ibo-input-select-icon--menu--icon--margin-right: 10px !default;
|
||||
display: inline-flex;
|
||||
text-align: left;
|
||||
>img{
|
||||
max-height: $ibo-input-select-icon--menu--icon--max-height;
|
||||
max-width: $ibo-input-select-icon--menu--icon--max-width;
|
||||
padding-right: $ibo-input-select-icon--icon--padding-right;
|
||||
max-height: $ibo-input-select-icon--max-height;
|
||||
max-width: $ibo-input-select-icon--max-width;
|
||||
padding-right: $ibo-input-select-icon--padding-right;
|
||||
}
|
||||
>span{
|
||||
overflow: hidden;
|
||||
|
||||
@@ -80,4 +80,14 @@ $ibo-data-synchro-source--synoptics--cell--arrow--border: 2px solid $ibo-data-sy
|
||||
}
|
||||
.ibo-data-synchro-source--replicas-status--warning{
|
||||
margin: $ibo-data-synchro-source--replicas-status--warning--margin;
|
||||
}
|
||||
|
||||
.ibo-page-header--replica-title{
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
||||
>.ibo-toolbar--button{
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *) {
|
||||
.ck-editor__editable_inline:not(.ck-comment__input *), .ck-source-editing-area {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@@ -84,11 +84,6 @@ $ibo-vendors-ckeditor--ck-mentions--item--padding-y: $ibo-spacing-200 !default;
|
||||
}
|
||||
}
|
||||
|
||||
// N°7552 Allow source editing area to be scrollable in full screen
|
||||
.ck-maximize_editor_main .ck-source-editing-area textarea{
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Mentions */
|
||||
.ck-mentions {
|
||||
.ck-button {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
/* SCSS variables */
|
||||
$common-has-description--content: "\1F6C8" !default;
|
||||
$common-has-description--content: "?" !default;
|
||||
$common-has-description--padding-left: $common-spacing-200 !default;
|
||||
$common-has-description--color: $common-color-grey-600 !default;
|
||||
$common-has-description--font-size: 0.7em !default; /* Font size is em on purpose as we want it to be proportional to its context */
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
*
|
||||
* Usage: @extend %common-font-size-XXX;
|
||||
*/
|
||||
%common-font-size-20 {
|
||||
font-size: $common-font-size-20;
|
||||
}
|
||||
%common-font-size-50 {
|
||||
font-size: $common-font-size-50;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -611,32 +611,6 @@ body {
|
||||
.setup-extension--missing .setup-extension--icon{
|
||||
color:#a00000;
|
||||
}
|
||||
.setup-extension-tag {
|
||||
display: inline-flex;
|
||||
background-color: grey;
|
||||
border-radius: 8px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
margin-right: 3px;
|
||||
&.installed{
|
||||
background-color:#9eff9e
|
||||
}
|
||||
&.notinstalled{
|
||||
background-color:#ed9eff
|
||||
}
|
||||
&.tobeinstalled{
|
||||
background-color:#9ef0ff
|
||||
}
|
||||
&.tobeuninstalled{
|
||||
background-color:#ff9e9e
|
||||
}
|
||||
&.notuninstallable{
|
||||
background-color:#ffc98c
|
||||
}
|
||||
&.removed{
|
||||
background-color: #969594
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-extension-details {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'CAS:Error:UserNotAllowed' => '用户被禁止登录',
|
||||
'CAS:Login:SignIn' => '使用CAS登录',
|
||||
'CAS:Login:SignInTooltip' => '点击这里使用CAS服务器认证',
|
||||
'CAS:Login:SignIn' => '使用 CAS 登录',
|
||||
'CAS:Login:SignInTooltip' => '点击这里使用 CAS 服务器认证',
|
||||
]);
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @author Robert Deng <denglx@gmail.com>
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -23,6 +21,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// Dictionnay conventions
|
||||
// Class:<class_name>
|
||||
// Class:<class_name>+
|
||||
@@ -32,9 +31,11 @@
|
||||
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>+
|
||||
|
||||
//
|
||||
// Class: UserExternal
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:UserExternal' => '外部用户',
|
||||
'Class:UserExternal+' => '用户在'.ITOP_APPLICATION_SHORT.'外部验证身份',
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:UserLDAP' => 'Пользователь LDAP',
|
||||
'Class:UserLDAP+' => 'Пользователь, аутентифицируемый через LDAP',
|
||||
'UserLDAP:server' => 'LDAP specifics~~',
|
||||
'UserLDAP:server' => 'Особенности LDAP',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -22,6 +22,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
//
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
|
||||
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
|
||||
'Class:UserLDAP/Attribute:ldap_server' => 'Сервер LDAP',
|
||||
'Class:UserLDAP/Attribute:ldap_server+' => '',
|
||||
]);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @author Robert Deng <denglx@gmail.com>
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
@@ -22,6 +21,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// Dictionnay conventions
|
||||
// Class:<class_name>
|
||||
// Class:<class_name>+
|
||||
@@ -31,13 +31,15 @@
|
||||
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>+
|
||||
|
||||
//
|
||||
// Class: UserLDAP
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:UserLDAP' => 'LDAP用户',
|
||||
'Class:UserLDAP+' => '用户身份由LDAP认证',
|
||||
'UserLDAP:server' => 'LDAP详情',
|
||||
'Class:UserLDAP' => 'LDAP 用户',
|
||||
'Class:UserLDAP+' => '用户身份由 LDAP 认证',
|
||||
'UserLDAP:server' => 'LDAP 详情',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -45,6 +47,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:UserLDAP/Attribute:ldap_server' => 'Ldap server~~',
|
||||
'Class:UserLDAP/Attribute:ldap_server+' => '~~',
|
||||
'Class:UserLDAP/Attribute:ldap_server' => 'LDAP 服务器',
|
||||
'Class:UserLDAP/Attribute:ldap_server+' => '',
|
||||
]);
|
||||
|
||||
@@ -24,11 +24,11 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:UserLocal/Attribute:expiration/Value:never_expire+' => '',
|
||||
'Class:UserLocal/Attribute:expiration/Value:force_expire' => 'Истёкший',
|
||||
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'One-time Password~~',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Password cannot be changed by the user.~~',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => 'Одноразовый пароль',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => 'Пароль не может быть изменён пользователем.',
|
||||
'Class:UserLocal/Attribute:password_renewed_date' => 'Дата изменения пароля',
|
||||
'Class:UserLocal/Attribute:password_renewed_date+' => 'Когда пароль был изменен в последний раз',
|
||||
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => 'Пароль должен содержать не менее 12 символов и включать прописные, строчные, числовые и специальные символы.',
|
||||
'UserLocal:password:expiration' => 'Поля требуют наличия доп. расширения',
|
||||
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Setting password expiration to "One-time password" is not allowed for your own User~~',
|
||||
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => 'Установка срока действия пароля "Одноразовый пароль" для своей собственной учётной записи не разрешена',
|
||||
]);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @author Robert Deng <denglx@gmail.com>
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
@@ -22,6 +21,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// Dictionnay conventions
|
||||
// Class:<class_name>
|
||||
// Class:<class_name>+
|
||||
@@ -31,16 +31,19 @@
|
||||
// Class:<class_name>/Attribute:<attribute_code>/Value:<value>+
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>
|
||||
// Class:<class_name>/Stimulus:<stimulus_code>+
|
||||
|
||||
//
|
||||
// Class: UserLocal
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:UserLocal' => ITOP_APPLICATION_SHORT.' 用户',
|
||||
'Class:UserLocal+' => '用户由'.ITOP_APPLICATION_SHORT.'验证身份',
|
||||
'Class:UserLocal/Attribute:password' => '密码',
|
||||
'Class:UserLocal/Attribute:password+' => '用于验证用户身份的字符串',
|
||||
'Class:UserLocal/Attribute:expiration' => '密码过期',
|
||||
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要一个扩展才能生效)',
|
||||
|
||||
'Class:UserLocal/Attribute:expiration' => '密码过期时间',
|
||||
'Class:UserLocal/Attribute:expiration+' => '密码过期状态 (需要扩展才能生效)',
|
||||
'Class:UserLocal/Attribute:expiration/Value:can_expire' => '允许过期',
|
||||
'Class:UserLocal/Attribute:expiration/Value:can_expire+' => '',
|
||||
'Class:UserLocal/Attribute:expiration/Value:never_expire' => '永不过期',
|
||||
@@ -49,8 +52,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:UserLocal/Attribute:expiration/Value:force_expire+' => '',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire' => '一次性密码',
|
||||
'Class:UserLocal/Attribute:expiration/Value:otp_expire+' => '用户不允许修改密码.',
|
||||
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新',
|
||||
'Class:UserLocal/Attribute:password_renewed_date' => '密码更新时间',
|
||||
'Class:UserLocal/Attribute:password_renewed_date+' => '上次修改密码的时间',
|
||||
|
||||
'Error:UserLocalPasswordValidator:UserPasswordPolicyRegex:ValidationFailed' => '密码必须至少12个字符, 包含大小写, 数字和特殊字符.',
|
||||
'UserLocal:password:expiration' => '下面的区域需要插件扩展',
|
||||
'Class:UserLocal/Error:OneTimePasswordChangeIsNotAllowed' => '不允许用户为自己设置 "一次性密码" 的失效期限',
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
*
|
||||
*/
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:darkmoon' => 'Dark moon~~',
|
||||
'theme:darkmoon' => 'Тёмная луна',
|
||||
]);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:darkmoon' => '暗月',
|
||||
'theme:darkmoon' => 'Dark moon',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (высокая контрастность)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (High contrast)~~',
|
||||
'theme:fullmoon-high-contrast' => 'Fullmoon (高对比度)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (протанопия и дейтеранопия)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (Protanopia & Deuteranopia)~~',
|
||||
'theme:fullmoon-protanopia-deuteranopia' => 'Fullmoon (红绿色盲 & 绿色色盲)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (тританопия)',
|
||||
]);
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (Tritanopia)~~',
|
||||
'theme:fullmoon-tritanopia' => 'Fullmoon (蓝黄色盲)',
|
||||
]);
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
|
||||
<classes>
|
||||
<class id="ResourceDataFeatureRemoval" _delta="define">
|
||||
<parent>AbstractResource</parent>
|
||||
<properties>
|
||||
<comment>/* Extension management access control. */</comment>
|
||||
<abstract>true</abstract>
|
||||
<category>grant_by_profile</category>
|
||||
</properties>
|
||||
<presentation/>
|
||||
<methods/>
|
||||
</class>
|
||||
</classes>
|
||||
<menus>
|
||||
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
|
||||
<rank>30</rank>
|
||||
<parent>SystemTools</parent>
|
||||
<url>$pages/exec.php?exec_module=combodo-data-feature-removal&exec_page=index.php&c[menu]=DataFeatureRemovalMenu</url>
|
||||
<enable_admin_only>1</enable_admin_only>
|
||||
<enable_class>ResourceDataFeatureRemoval</enable_class>
|
||||
<enable_action>UR_ACTION_MODIFY</enable_action>
|
||||
</menu>
|
||||
</menus>
|
||||
<module_parameters>
|
||||
|
||||
@@ -20,6 +20,7 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification requested',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'No modification requested',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Deletion Executions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Review all elements requiring attention',
|
||||
@@ -39,6 +40,14 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation in progress...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation successful',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Compilation error',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analysis in progress...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analysis completed',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Error during analysis',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyze',
|
||||
'UI:Button:ModifyChoices' => 'Change my selection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
|
||||
|
||||
@@ -20,6 +20,7 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Extensions',
|
||||
'DataFeatureRemoval:Result:Title' => 'Modification demandée',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'Aucune modification demandée',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Suppressions',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Résultat de l’analyse',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Vérifier les éléments à nettoyer',
|
||||
@@ -39,6 +40,14 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'All clear.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Compilation successful. No Cleanup needed. You can proceed to setup.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Compilation en cours...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Compilation terminée',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Erreur lors de la compilation',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Analyse en cours...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Analyse terminée',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Erreur lors de l\'analyse',
|
||||
|
||||
'UI:Button:Analyze' => 'Analyser',
|
||||
'UI:Button:ModifyChoices' => 'Modifier la sélection',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir l’assistant de configuration',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Localized data
|
||||
*/
|
||||
/**
|
||||
* @author Vladimir Kunin <v.b.kunin@gmail.com>
|
||||
*
|
||||
*/
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Menu:DataFeatureRemovalMenu' => 'Управление расширениями',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => 'Управление расширениями',
|
||||
|
||||
'DataFeatureRemoval:Main:Title' => 'Управление расширениями',
|
||||
'DataFeatureRemoval:Main:SubTitle' => 'Включение и отключение расширений, установленных в вашем iTop',
|
||||
'DataFeatureRemoval:Failure:Title' => 'Ошибки пробного удаления расширений',
|
||||
'DataFeatureRemoval:Helper:Title' => 'Проверьте, есть ли данные или зависимости, мешающие добавить/удалить расширение.',
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => 'Расширения',
|
||||
'DataFeatureRemoval:Result:Title' => 'Запрошено изменение',
|
||||
'DataFeatureRemoval:NoResult:Title' => 'Изменений не запрошено',
|
||||
'DataFeatureRemoval:Execution:Title' => 'Выполнения удаления',
|
||||
'DataFeatureRemoval:Analysis:Title' => 'Результат анализа',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => 'Просмотрите все элементы, требующие внимания',
|
||||
'DataFeatureRemoval:Analysis:SubTitle' => 'Элементов для очистки перед продолжением: %1$s',
|
||||
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => 'План удаления данных',
|
||||
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Строк для очистки перед продолжением: %1$s',
|
||||
'DataFeatureRemoval:DoDeletion:Title' => 'Выполнить удаление',
|
||||
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Удалить все записи из базы данных',
|
||||
'DataFeatureRemoval:DeletionPlan:Error:Issues' => 'Некоторые объекты нужно удалить вручную перед очисткой',
|
||||
|
||||
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Элемент для удаления',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Название расширения',
|
||||
'DataFeatureRemoval:Table:Analysis:Module' => 'Название модуля',
|
||||
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Количество',
|
||||
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => 'Всё чисто.',
|
||||
'DataFeatureRemoval:CompilComplete' => 'Компиляция выполнена успешно. Очистка не требуется. Можно переходить к установке.',
|
||||
|
||||
'DataFeatureRemoval:Compile:InProgress' => 'Идёт компиляция...',
|
||||
'DataFeatureRemoval:Compile:Success' => 'Компиляция выполнена успешно',
|
||||
'DataFeatureRemoval:Compile:Error' => 'Ошибка компиляции',
|
||||
|
||||
'DataFeatureRemoval:RunAudit:InProgress' => 'Идёт анализ...',
|
||||
'DataFeatureRemoval:RunAudit:Success' => 'Анализ завершён',
|
||||
'DataFeatureRemoval:RunAudit:Error' => 'Ошибка при анализе',
|
||||
|
||||
'UI:Button:Analyze' => 'Анализировать',
|
||||
'UI:Button:ModifyChoices' => 'Изменить выбор',
|
||||
'UI:Button:AnalyzeAndSetup' => 'Анализировать и перейти к установке',
|
||||
'UI:Button:PlanDeletion' => 'Продолжить удаление',
|
||||
'UI:Button:DoDeletion' => 'Продолжить удаление',
|
||||
'UI:Button:BackToMain' => 'Изменить выбор',
|
||||
'UI:Button:Setup' => 'Запустить установку',
|
||||
|
||||
'UI:Action:ForceUninstall' => 'Принудительно удалить',
|
||||
'UI:Action:MoreInfo' => 'Подробнее',
|
||||
|
||||
'DataFeatureRemoval:Table:Empty' => 'Нет данных для удаления',
|
||||
|
||||
'DataFeatureRemoval:Column:Class' => 'Класс',
|
||||
'DataFeatureRemoval:Column:DeleteCount' => 'Записей к удалению',
|
||||
'DataFeatureRemoval:Column:UpdateCount' => 'Записей к обновлению',
|
||||
'DataFeatureRemoval:Column:IssueCount' => 'Найдено проблем, мешающих автоматической очистке',
|
||||
|
||||
'DataFeatureRemoval:Column:DeletedCount' => 'Удалено записей',
|
||||
'DataFeatureRemoval:Column:UpdatedCount' => 'Обновлено записей',
|
||||
]);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Localized data
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Menu:DataFeatureRemovalMenu' => '扩展管理',
|
||||
'combodo-data-feature-removal/Operation:Main/Title' => '扩展管理',
|
||||
|
||||
'DataFeatureRemoval:Main:Title' => '扩展管理',
|
||||
'DataFeatureRemoval:Main:SubTitle' => '切换安装在您的 iTop 上的扩展',
|
||||
'DataFeatureRemoval:Failure:Title' => '扩展预删除错误',
|
||||
'DataFeatureRemoval:Helper:Title' => '分析是否有任何数据或依赖关系阻止您添加/删除扩展。',
|
||||
|
||||
'DataFeatureRemoval:Features:Title' => '扩展',
|
||||
'DataFeatureRemoval:Result:Title' => '请求的修改',
|
||||
'DataFeatureRemoval:Execution:Title' => '删除执行',
|
||||
'DataFeatureRemoval:Analysis:Title' => '分析结果',
|
||||
'DataFeatureRemoval:Analysis:Subtitle' => '审查所有需要关注的元素',
|
||||
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s 个元素需要在继续之前清理',
|
||||
|
||||
'DataFeatureRemoval:DeletionPlan:Title' => '数据删除计划',
|
||||
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s 行需要在继续之前清理',
|
||||
'DataFeatureRemoval:DoDeletion:Title' => '执行删除',
|
||||
'DataFeatureRemoval:DoDeletion:SubTitle' => '从数据库中删除所有条目',
|
||||
'DataFeatureRemoval:DeletionPlan:Error:Issues' => '某些对象必须在清理前手动删除',
|
||||
|
||||
'DataFeatureRemoval:Table:Analysis:ClassName' => '要删除的元素',
|
||||
'DataFeatureRemoval:Table:Analysis:FeatureName' => '扩展名称',
|
||||
'DataFeatureRemoval:Table:Analysis:Module' => '模块名称',
|
||||
'DataFeatureRemoval:Table:Analysis:Occurrence' => '出现次数',
|
||||
|
||||
'DataFeatureRemoval:CleanupComplete:Title' => '全部清除.',
|
||||
'DataFeatureRemoval:CompilComplete' => '编译成功. 无需清理. 您可以继续进行设置.',
|
||||
|
||||
'UI:Button:Analyze' => '分析',
|
||||
'UI:Button:ModifyChoices' => '改变我的选择',
|
||||
'UI:Button:AnalyzeAndSetup' => '分析并进入设置',
|
||||
'UI:Button:PlanDeletion' => '继续删除',
|
||||
'UI:Button:DoDeletion' => '继续删除',
|
||||
'UI:Button:BackToMain' => '改变我的选择',
|
||||
'UI:Button:Setup' => '运行安装向导',
|
||||
|
||||
'UI:Action:ForceUninstall' => '强制卸载',
|
||||
'UI:Action:MoreInfo' => '更多信息',
|
||||
|
||||
'DataFeatureRemoval:Table:Empty' => '没有数据需要删除',
|
||||
|
||||
'DataFeatureRemoval:Column:Class' => '类',
|
||||
'DataFeatureRemoval:Column:DeleteCount' => '待删除的条目',
|
||||
'DataFeatureRemoval:Column:UpdateCount' => '待更新的条目',
|
||||
'DataFeatureRemoval:Column:IssueCount' => '发现阻止自动清理的问题',
|
||||
|
||||
'DataFeatureRemoval:Column:DeletedCount' => '已删除的条目',
|
||||
'DataFeatureRemoval:Column:UpdatedCount' => '已更新的条目',
|
||||
]);
|
||||
@@ -8,7 +8,6 @@
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Controller;
|
||||
|
||||
require_once APPROOT.'setup/feature_removal/SetupAudit.php';
|
||||
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
|
||||
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
@@ -18,15 +17,16 @@ use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
|
||||
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalLog;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DataCleanupService;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
|
||||
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
|
||||
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
|
||||
use Combodo\iTop\Service\Session\SessionParameters;
|
||||
use Combodo\iTop\Setup\FeatureRemoval\SetupAudit;
|
||||
use ContextTag;
|
||||
use CoreException;
|
||||
use Dict;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use MFCompiler;
|
||||
use RunTimeEnvironment;
|
||||
use SecurityException;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
@@ -37,15 +37,22 @@ class DataFeatureRemovalController extends Controller
|
||||
private array $aCountClassesToCleanup = [];
|
||||
private array $aAnalysisDataTable = [];
|
||||
private array $aDeletionExecutionSummary = [];
|
||||
private ?RuntimeEnvironment $oRuntimeEnvironment = null;
|
||||
|
||||
private int $iCount = 0;
|
||||
private int $iColumnCount = 2;
|
||||
private RunTimeEnvironment $oRuntimeEnvironment;
|
||||
|
||||
public function OperationMain($sErrorMessage = null): void
|
||||
{
|
||||
$aParams = [];
|
||||
|
||||
SetupUtils::EraseSetupToken();
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$oParameters->Erase();
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
Session::Set('bForceCompilation', true);
|
||||
$oParameters->SetParameter('return_application', 'DataFeatureRemoval');
|
||||
|
||||
$this->AddAnalyzeParams();
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['iColumnCount'] = $this->iColumnCount;
|
||||
@@ -57,7 +64,6 @@ class DataFeatureRemovalController extends Controller
|
||||
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
|
||||
$aParams['iCount'] = $this->iCount;
|
||||
|
||||
Session::Set('bForceCompilation', true);
|
||||
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
|
||||
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
|
||||
$this->DisplayPage($aParams);
|
||||
@@ -84,164 +90,184 @@ class DataFeatureRemovalController extends Controller
|
||||
{
|
||||
$aParams = [];
|
||||
|
||||
try {
|
||||
//from setup wizard/mtp
|
||||
SetupUtils::CheckSetupToken();
|
||||
SetupUtils::EraseSetupToken();
|
||||
} catch (SecurityException $e) {
|
||||
//from setup wizard/mtp
|
||||
if (!SetupUtils::IsSessionSetupTokenValid()) {
|
||||
//from same module
|
||||
$this->ValidateTransactionId();
|
||||
}
|
||||
|
||||
// Display changed extensions
|
||||
$aHiddenInputNames = [
|
||||
$aSetupParameterNames = [
|
||||
'selected_extensions' => '[]',
|
||||
'selected_modules' => '[]',
|
||||
'display_choices' => '[]',
|
||||
'display_choices' => '',
|
||||
'added_extensions' => '[]',
|
||||
'removed_extensions' => '[]',
|
||||
'extensions_not_uninstallable' => '[]',
|
||||
'copy_setup_files' => 1,
|
||||
'return_button_label' => '',
|
||||
'force-uninstall' => '',
|
||||
'use_symbolic_links' => MFCompiler::UseSymbolicLinks() ? 'on' : '',
|
||||
'return_application' => '',
|
||||
'target_env' => ITOP_DEFAULT_ENV,
|
||||
];
|
||||
|
||||
$aHiddenInputs = [];
|
||||
foreach ($aHiddenInputNames as $sInputName => $defaultValue) {
|
||||
$aHiddenInputs[$sInputName] = utils::ReadPostedParam($sInputName, $defaultValue, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
}
|
||||
$aParams['aHiddenInputs'] = $aHiddenInputs;
|
||||
|
||||
if ($aHiddenInputs['return_button_label'] !== '') {
|
||||
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
foreach ($aSetupParameterNames as $sInputName => $defaultValue) {
|
||||
$oParameters->SetParameter($sInputName, $oParameters->GetParameter($sInputName, $defaultValue));
|
||||
}
|
||||
|
||||
$aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true);
|
||||
|
||||
$aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true);
|
||||
if ("[]" === $aHiddenInputs['selected_modules']) {
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
if ('[]' === $oParameters->GetParameter('selected_modules', '[]')) {
|
||||
//it does not come from setup
|
||||
// we get extensions from 1st screen uiblocks
|
||||
$this->ReadExtensionsDiff();
|
||||
$oParameters->SetParameter('force-uninstall', $this->bForcedUninstallation ? 'on' : '');
|
||||
$aAddedExtensions = $this->aExtensionsToCheck['to_be_installed'];
|
||||
$aHiddenInputs['added_extensions'] = $this->ConvertIntoSetupFormat($aAddedExtensions);
|
||||
$oParameters->SetParameter('added_extensions', $this->ConvertIntoSetupFormat($aAddedExtensions));
|
||||
|
||||
$aRemovedExtensions = $this->aExtensionsToCheck['to_be_removed'];
|
||||
$aHiddenInputs['removed_extensions'] = $this->ConvertIntoSetupFormat($aRemovedExtensions);
|
||||
$oParameters->SetParameter('removed_extensions', $this->ConvertIntoSetupFormat($aRemovedExtensions));
|
||||
|
||||
$aExtensionsNotUninstallable = $this->aExtensionsToCheck['extensions_not_uninstallable'];
|
||||
$oParameters->SetParameter('extensions_not_uninstallable', $this->ConvertIntoSetupFormat($aExtensionsNotUninstallable));
|
||||
}
|
||||
|
||||
$aRemoveExtensionCodes = array_keys($aRemovedExtensions);
|
||||
|
||||
$aParams['aAddedExtensions'] = $aAddedExtensions;
|
||||
$aParams['aRemovedExtensions'] = $aRemovedExtensions;
|
||||
|
||||
DataFeatureRemovalLog::Debug(__METHOD__.' Extensions given in parameter', null, [
|
||||
'added_extensions' => $aAddedExtensions,
|
||||
'removed_extensions' => $aRemovedExtensions]);
|
||||
|
||||
$aParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
$aParams['iColumnCount'] = $this->iColumnCount;
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions), $this->iColumnCount);
|
||||
|
||||
$bForceCompilation = Session::Get('bForceCompilation', false);
|
||||
try {
|
||||
$this->Compile($aRemoveExtensionCodes, $bForceCompilation);
|
||||
} catch (CoreException $e) {
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $e->getHtmlDesc();
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
$aParams['DataFeatureRemovalErrorMessage'] = $e->getMessage();
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
return;
|
||||
}
|
||||
|
||||
if ("[]" === $aHiddenInputs['selected_modules']) {
|
||||
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, $aAddedExtensions, $aRemovedExtensions);
|
||||
$aHiddenInputs['selected_extensions'] = $this->ConvertIntoSetupFormat($aSelectedExtensions);
|
||||
|
||||
$oRunTimeEnvironment = $this->GetRuntimeEnvironment($aRemovedExtensions);
|
||||
$aSearchDirs = [$oRunTimeEnvironment->GetBuildDir()];
|
||||
$aSelectedModules = $oRunTimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions, $aSearchDirs);
|
||||
$aHiddenInputs['selected_modules'] = $this->ConvertIntoSetupFormat($aSelectedModules);
|
||||
}
|
||||
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aGetRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
|
||||
DataFeatureRemovalLog::Debug(__METHOD__, null, ['aGetRemovedClasses' => $aGetRemovedClasses]);
|
||||
|
||||
$aParams['aClasses'] = $aGetRemovedClasses;
|
||||
|
||||
new ContextTag(ContextTag::TAG_SETUP);
|
||||
$aParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
|
||||
$aParams['aSetupParams'] = [
|
||||
"_class" => "WizStepLandingBeforeAudit",
|
||||
"operation" => "next",
|
||||
"_params[authent]" => SetupUtils::CreateSetupToken(),
|
||||
];
|
||||
|
||||
foreach ($aHiddenInputs as $sInputName => $sInputValue) {
|
||||
$aParams['aSetupParams']["_params[$sInputName]"] = $sInputValue;
|
||||
}
|
||||
|
||||
if ($aHiddenInputs['return_button_label'] !== '') {
|
||||
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
}
|
||||
|
||||
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
|
||||
[$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0);
|
||||
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
|
||||
$aAvailableExtensions = $this->GetExtensionsDiff($aAddedExtensions, $aRemovedExtensions);
|
||||
$aParams['aAvailableExtensionsCount'] = count($aAvailableExtensions);
|
||||
$aParams['aAvailableExtensions'] = $this->SplitArrayIntoColumns($aAvailableExtensions, $this->iColumnCount);
|
||||
$aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage(DataFeatureRemovalHelper::MODULE_NAME, 'index.php');
|
||||
|
||||
$this->DisplayPage($aParams, 'AnalysisResult');
|
||||
}
|
||||
|
||||
private function ConvertIntoSetupFormat(array $aData): string
|
||||
{
|
||||
return json_encode($aData);
|
||||
$aNewData = [];
|
||||
foreach ($aData as $sCode => $sLabel) {
|
||||
$aNewData[] = sprintf('"%s":"%s"', $sCode, $sLabel);
|
||||
}
|
||||
|
||||
return "{".implode(',', $aNewData)."}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aRemovedExtensions
|
||||
* @param bool $bForceCompilation
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
private function Compile(array $aRemovedExtensions, bool $bForceCompilation = true): void
|
||||
public function OperationAjaxCompile(): void
|
||||
{
|
||||
$aParams = [];
|
||||
//to make setup redirection work, we need to pass complex data structures to setup wizards (ie extension/module lists)
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
|
||||
if (! is_dir($sBuildDir)) {
|
||||
SetupUtils::builddir($sBuildDir);
|
||||
}
|
||||
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
|
||||
$oRuntimeEnvironment = new RunTimeEnvironment($sSourceEnv, false);
|
||||
|
||||
if ($bIsDirEmpty || $bForceCompilation) {
|
||||
DataFeatureRemovalLog::Debug(
|
||||
__METHOD__,
|
||||
null,
|
||||
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
|
||||
);
|
||||
$this->GetRuntimeEnvironment($aRemovedExtensions)->CompileFrom($sSourceEnv);
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aSelectedModules = json_decode($oParameters->GetParameter('selected_modules', '[]'), true);
|
||||
$aSelectedExtensions = json_decode($oParameters->GetParameter('selected_extensions', '[]'), true);
|
||||
$aAddedExtensions = json_decode($oParameters->GetParameter('added_extensions', '[]'), true);
|
||||
$aRemovedExtensions = json_decode($oParameters->GetParameter('removed_extensions', '[]'), true);
|
||||
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedExtensions = DataFeatureRemoverExtensionService::GetInstance()->GetExtensionMap()->GetSelectedExtensions($oConfig, array_keys($aAddedExtensions), array_keys($aRemovedExtensions));
|
||||
$oParameters->SetParameter('selected_extensions', $this->ConvertIntoSetupFormat($aSelectedExtensions));
|
||||
}
|
||||
|
||||
$sBuildDir = APPROOT."/env-$sSourceEnv-build";
|
||||
if (! is_dir($sBuildDir)) {
|
||||
SetupUtils::builddir($sBuildDir);
|
||||
}
|
||||
$bIsDirEmpty = count(scandir($sBuildDir)) === 2;
|
||||
$bForceCompilation = Session::Get('bForceCompilation', false);
|
||||
if ($bIsDirEmpty || $bForceCompilation) {
|
||||
$oRuntimeEnvironment->CopySetupFiles();
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
|
||||
DataFeatureRemovalLog::Debug(
|
||||
__METHOD__,
|
||||
null,
|
||||
['sSourceEnv' => $sSourceEnv, 'sBuildDir' => $sBuildDir, 'bIsDirEmpty' => $bIsDirEmpty, glob("$sBuildDir/*")]
|
||||
);
|
||||
$oRuntimeEnvironment->DoCompile($aSelectedExtensions, $aRemovedExtensions, $aSelectedModules, MFCompiler::CanUseSymbolicLinks());
|
||||
Session::Unset('bForceCompilation');
|
||||
} else {
|
||||
if (count($aSelectedModules) === 0) {
|
||||
$aSelectedModules = $oRuntimeEnvironment->GetModulesToLoadFromChoices($oConfig, $aSelectedExtensions);
|
||||
}
|
||||
}
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
$aParams['success_message'] = Dict::S('DataFeatureRemoval:Compile:Success');
|
||||
$aParams['transaction_id'] = utils::GetNewTransactionId();
|
||||
|
||||
$oParameters->SetParameter('selected_modules', json_encode($aSelectedModules));
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
private function GetRuntimeEnvironment(array $aRemovedExtensions): RunTimeEnvironment
|
||||
public function OperationAjaxRunAudit(): void
|
||||
{
|
||||
if (is_null($this->oRuntimeEnvironment)) {
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aParams = [];
|
||||
$aPageParams = [];
|
||||
try {
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$sSourceEnv = MetaModel::GetEnvironment();
|
||||
$this->oRuntimeEnvironment = new DryRemovalRuntimeEnvironment($sSourceEnv, $aRemovedExtensions);
|
||||
$oSetupAudit = new SetupAudit($sSourceEnv);
|
||||
$aRemovedClasses = array_keys($oSetupAudit->RunDataAudit());
|
||||
$oParameters->SetParameter('classes', $aRemovedClasses);
|
||||
|
||||
new ContextTag(ContextTag::TAG_SETUP);
|
||||
$aPageParams['sLaunchSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup/wizard.php';
|
||||
$aPageParams['aSetupParams'] = [
|
||||
"_class" => "WizStepLandingBeforeAudit",
|
||||
"operation" => "next",
|
||||
];
|
||||
$aPageParams['sTransactionId'] = utils::GetNewTransactionId();
|
||||
|
||||
$aDeletionPlanSummaryEntities = $this->GetDeletionPlanSummaryEntities($aRemovedClasses);
|
||||
[$aPageParams['aDeletionPlanSummary'], $aPageParams['iQueryCount'], $aPageParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aDeletionPlanSummaryEntities);
|
||||
[$aPageParams['aDeletionExecutionSummary'], $aPageParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aPageParams['bDeletionNeeded'] = ($aPageParams['iQueryCount'] > 0);
|
||||
|
||||
if (!$aPageParams['bDeletionNeeded']) {
|
||||
// Erase session setup parameters
|
||||
SetupUtils::CreateSetupToken();
|
||||
}
|
||||
|
||||
$this->DisplayAjaxPage($aPageParams, 'AjaxRunAudit');
|
||||
return;
|
||||
} catch (CoreException $e) {
|
||||
$aParams['error_message'] = $e->getHtmlDesc();
|
||||
} catch (Exception $e) {
|
||||
$aParams['error_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $this->oRuntimeEnvironment;
|
||||
$this->DisplayJSONPage($aParams);
|
||||
}
|
||||
|
||||
private function GetExecutionSummaryTable(): array
|
||||
{
|
||||
$sName = 'ExcutionSummary';
|
||||
$sName = 'ExecutionSummary';
|
||||
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
|
||||
$aTableData = [];
|
||||
if (count($this->aDeletionExecutionSummary) === 0) {
|
||||
@@ -267,11 +293,15 @@ class DataFeatureRemovalController extends Controller
|
||||
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryTable(array $aRemovedClasses): array
|
||||
private function GetDeletionPlanSummaryEntities(array $aRemovedClasses): array
|
||||
{
|
||||
$oDataCleanupService = new StaticDeletionPlan();
|
||||
return $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
|
||||
}
|
||||
|
||||
private function GetDeletionPlanSummaryTable(array $aDeletionPlanSummaryEntities): array
|
||||
{
|
||||
$sName = 'DeletionPlanSummary';
|
||||
$oDataCleanupService = new DataCleanupService();
|
||||
$aDeletionPlanSummaryEntities = $oDataCleanupService->GetCleanupSummary($aRemovedClasses);
|
||||
$aColumns = ['Class', 'Delete Count' , 'Update Count', 'Issue Count'];
|
||||
$aRows = [];
|
||||
$iQueryCount = 0;
|
||||
@@ -294,9 +324,10 @@ class DataFeatureRemovalController extends Controller
|
||||
{
|
||||
$this->ValidateTransactionId();
|
||||
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary'));
|
||||
$this->aDeletionExecutionSummary = unserialize(Session::Get('aDeletionExecutionSummary') ?? serialize([]));
|
||||
Session::Unset('aDeletionExecutionSummary');
|
||||
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
|
||||
$oParameters = new SessionParameters(SetupUtils::SESSION_PARAMETERS_NAME);
|
||||
$aClasses = $oParameters->GetParameter('classes', []);
|
||||
|
||||
$oDataCleanupService = new DataCleanupService();
|
||||
$aDeletionExecutionSummary = $oDataCleanupService->ExecuteCleanup($aClasses);
|
||||
@@ -311,6 +342,7 @@ class DataFeatureRemovalController extends Controller
|
||||
$oSummary->iTotalUpdateCount += $oExecutionSummary->iUpdateCount;
|
||||
}
|
||||
|
||||
Session::Set('aDeletionExecutionSummary', serialize($this->aDeletionExecutionSummary));
|
||||
$this->OperationAnalysisResult();
|
||||
}
|
||||
|
||||
@@ -336,6 +368,7 @@ class DataFeatureRemovalController extends Controller
|
||||
'uninstallable' => $oExtension->CanBeUninstalled(),
|
||||
'remote' => $oExtension->IsRemote(),
|
||||
'missing' => $oExtension->bRemovedFromDisk,
|
||||
'dependency_issue' => $oExtension->HasDependencyIssue(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -403,12 +436,11 @@ class DataFeatureRemovalController extends Controller
|
||||
|
||||
/**
|
||||
* Read extensions selected from posted parameters
|
||||
* @return int Number of extensions to be added or removed
|
||||
*/
|
||||
public function ReadExtensionsDiff(): int
|
||||
public function ReadExtensionsDiff(): void
|
||||
{
|
||||
if (!is_null($this->aExtensionsToCheck)) {
|
||||
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
|
||||
return;
|
||||
}
|
||||
|
||||
$aAvailableExtensions = $this->GetAvailableExtensions();
|
||||
@@ -416,6 +448,7 @@ class DataFeatureRemovalController extends Controller
|
||||
$this->aExtensionsToCheck = [
|
||||
'to_be_installed' => [],
|
||||
'to_be_removed' => [],
|
||||
'extensions_not_uninstallable' => [],
|
||||
];
|
||||
foreach ($aAvailableExtensions as $sCode => &$aExtensionData) {
|
||||
if (!isset($aSelectedExtensionsFromUI[$sCode])) {
|
||||
@@ -426,16 +459,18 @@ class DataFeatureRemovalController extends Controller
|
||||
$aExtensionData['extra_flags']['selected'] = false;
|
||||
$sLabel = $aAvailableExtensions[$sCode]['label'];
|
||||
$this->aExtensionsToCheck['to_be_removed'][$sCode] = $sLabel;
|
||||
if (!$aExtensionData['extra_flags']['uninstallable'] || $aExtensionData['extra_flags']['remote']) {
|
||||
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
|
||||
$this->bForcedUninstallation = true;
|
||||
}
|
||||
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
|
||||
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
|
||||
}
|
||||
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {
|
||||
$aExtensionData['extra_flags']['selected'] = true;
|
||||
$sLabel = $aAvailableExtensions[$sCode]['label'];
|
||||
$this->aExtensionsToCheck['to_be_installed'][$sCode] = $sLabel;
|
||||
}
|
||||
}
|
||||
return count($this->aExtensionsToCheck['to_be_installed']) + count($this->aExtensionsToCheck['to_be_removed']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Entity;
|
||||
|
||||
class DeletionPlanEntity
|
||||
{
|
||||
public readonly DeletionPlanItem $oDelete;
|
||||
public readonly DeletionPlanItem $oUpdate;
|
||||
public readonly DeletionPlanItem $oIssue;
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oDelete
|
||||
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oUpdate
|
||||
* @param \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem $oIssue
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->oDelete = $oDelete ?? new DeletionPlanItem();
|
||||
$this->oUpdate = $oUpdate ?? new DeletionPlanItem();
|
||||
$this->oIssue = $oIssue ?? new DeletionPlanItem();
|
||||
}
|
||||
|
||||
public function TotalCount(): int
|
||||
{
|
||||
return $this->oDelete->Count() + $this->oUpdate->Count() + $this->oIssue->Count();
|
||||
}
|
||||
|
||||
public function FilterUpdatesByDeletes()
|
||||
{
|
||||
$this->oUpdate->FilterBy($this->oDelete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Entity;
|
||||
|
||||
class DeletionPlanItem
|
||||
{
|
||||
public array $aIds = [];
|
||||
|
||||
/**
|
||||
* @param array $aIds
|
||||
*/
|
||||
public function __construct(array $aIds = [])
|
||||
{
|
||||
$this->aIds = $aIds;
|
||||
}
|
||||
|
||||
public function Merge(DeletionPlanItem $oItem): void
|
||||
{
|
||||
$this->aIds = array_unique(array_merge($this->aIds, $oItem->aIds));
|
||||
}
|
||||
|
||||
public function Count(): int
|
||||
{
|
||||
return count($this->aIds);
|
||||
}
|
||||
|
||||
public function FilterBy(DeletionPlanItem $oItem): void
|
||||
{
|
||||
$this->aIds = array_diff($this->aIds, $oItem->aIds);
|
||||
}
|
||||
}
|
||||
@@ -134,10 +134,6 @@ class DataCleanupService
|
||||
/** @var DBObject $oDependentObj */
|
||||
while ($oDependentObj = $oSet->Fetch()) {
|
||||
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
$this->oObjectService->SetIssue(get_class($oDependentObj));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($oExtKeyAttDef->IsNullAllowed()) {
|
||||
// Optional external key, list to reset
|
||||
@@ -152,6 +148,12 @@ class DataCleanupService
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Mandatory external key
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
// Cannot be deleted automatically, must be handled manually
|
||||
$this->oObjectService->SetIssue(get_class($oDependentObj));
|
||||
continue;
|
||||
}
|
||||
// Propagate deletion only if not visited
|
||||
if ($this->IsVisited($oDependentObj)) {
|
||||
continue;
|
||||
|
||||
@@ -66,7 +66,7 @@ class DataFeatureRemoverExtensionService
|
||||
public function GetExtensionMap(): iTopExtensionsMap
|
||||
{
|
||||
if (is_null($this->oMap)) {
|
||||
$this->oMap = new iTopExtensionsMap();
|
||||
$this->oMap = iTopExtensionsMap::GetExtensionsMap();
|
||||
$this->oMap->LoadInstalledExtensionsFromDatabase(MetaModel::GetConfig());
|
||||
}
|
||||
return $this->oMap;
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\DataFeatureRemoval\Service;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanEntity;
|
||||
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem;
|
||||
use MetaModel;
|
||||
|
||||
class StaticDeletionPlan
|
||||
{
|
||||
/** @var array<DeletionPlanEntity> */
|
||||
private array $aDeletionPlan = [];
|
||||
|
||||
/**
|
||||
* Get a summary of the deletion plan computed for the classes.
|
||||
* The result is used for display
|
||||
*
|
||||
* @param array|null $aClasses
|
||||
*
|
||||
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity>
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function GetCleanupSummary(?array $aClasses): array
|
||||
{
|
||||
$aSummary = [];
|
||||
$aDeletionPlan = $this->GetStaticDeletionPlan($aClasses ?? []);
|
||||
|
||||
foreach ($aDeletionPlan as $sClass => $oDeletionPlanEntity) {
|
||||
if ($oDeletionPlanEntity->TotalCount() === 0) {
|
||||
continue;
|
||||
}
|
||||
$oDeletionPlanEntity->FilterUpdatesByDeletes();
|
||||
$oDataCleanupSummary = new DataCleanupSummaryEntity($sClass);
|
||||
$oDataCleanupSummary->iUpdateCount = $oDeletionPlanEntity->oUpdate->Count();
|
||||
$oDataCleanupSummary->iDeleteCount = $oDeletionPlanEntity->oDelete->Count();
|
||||
$oDataCleanupSummary->iIssueCount = $oDeletionPlanEntity->oIssue->Count();
|
||||
|
||||
$aSummary[$sClass] = $oDataCleanupSummary;
|
||||
}
|
||||
|
||||
return $aSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aClasses Classes to clean entirely
|
||||
*
|
||||
* @return array ['class' => DeletionPlanEntity];
|
||||
*
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function GetStaticDeletionPlan(array $aClasses): array
|
||||
{
|
||||
foreach ($aClasses as $sClass) {
|
||||
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
|
||||
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
|
||||
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
|
||||
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
|
||||
}
|
||||
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
|
||||
|
||||
$this->DeletionPlanForReferencingClasses($sClass);
|
||||
}
|
||||
|
||||
return $this->aDeletionPlan;
|
||||
}
|
||||
|
||||
private function DeletionPlanForReferencingClasses(string $sClass): void
|
||||
{
|
||||
$sIdsToRemove = implode(', ', $this->aDeletionPlan[$sClass]->oDelete->aIds);
|
||||
$aReferencingMe = MetaModel::EnumReferencingClasses($sClass);
|
||||
foreach ($aReferencingMe as $sRemoteClass => $aExtKeys) {
|
||||
if (!isset($this->aDeletionPlan[$sRemoteClass])) {
|
||||
$this->aDeletionPlan[$sRemoteClass] = new DeletionPlanEntity();
|
||||
}
|
||||
$oDeletionPlanEntity = $this->aDeletionPlan[$sRemoteClass];
|
||||
/** @var \AttributeExternalKey $oExtKeyAttDef */
|
||||
foreach ($aExtKeys as $sExtKeyAttCode => $oExtKeyAttDef) {
|
||||
// skip if this external key is behind an external field
|
||||
if (!$oExtKeyAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($oExtKeyAttDef->IsNullAllowed()) {
|
||||
// update
|
||||
$oUpdateItem = $this->UpdateExtKeyNullable($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove);
|
||||
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
|
||||
} else {
|
||||
// delete
|
||||
$aRemoteIdsToRemove = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemove);
|
||||
|
||||
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
|
||||
if ($iDeletePropagationOption == DEL_MANUAL) {
|
||||
// Issue, do not recurse
|
||||
$oDeletionPlanItem = new DeletionPlanItem(aIds: $aRemoteIdsToRemove);
|
||||
$oDeletionPlanEntity->oIssue->Merge($oDeletionPlanItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($iDeletePropagationOption == DEL_MOVEUP) && ($oExtKeyAttDef->IsHierarchicalKey())) {
|
||||
// update hierarchical keys due to row cleanup in the same table
|
||||
$sTargetIdsToRemove = implode(',', $this->aDeletionPlan[$sRemoteClass]->oDelete->aIds);
|
||||
$oUpdateItem = $this->UpdateHierarchicalExtKey($sRemoteClass, $sExtKeyAttCode, $sTargetIdsToRemove);
|
||||
$oDeletionPlanEntity->oUpdate->Merge($oUpdateItem);
|
||||
}
|
||||
|
||||
// Delete entries in Remote Class
|
||||
if (count($aRemoteIdsToRemove) !== 0) {
|
||||
$oDeletionPlanEntity->oDelete->Merge(new DeletionPlanItem($aRemoteIdsToRemove));
|
||||
// Infinite loops do not occurs due to the datamodel structure
|
||||
$this->DeletionPlanForReferencingClasses($sRemoteClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRemoteClass
|
||||
* @param string $sExtKeyAttCode
|
||||
* @param string $sIdsToRemoveInTargetClass
|
||||
*
|
||||
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function UpdateExtKeyNullable(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
|
||||
{
|
||||
$aIds = $this->GetRemoteIdsForExtKey($sRemoteClass, $sExtKeyAttCode, $sIdsToRemoveInTargetClass);
|
||||
|
||||
return new DeletionPlanItem($aIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRemoteClass
|
||||
* @param string $sExtKeyAttCode
|
||||
* @param string $sIdsToRemoveInTargetClass
|
||||
*
|
||||
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function UpdateHierarchicalExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): DeletionPlanItem
|
||||
{
|
||||
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
|
||||
|
||||
$sSQL = <<<SQL
|
||||
SELECT `$sDBKey`
|
||||
FROM `$sDBTable` AS `updated`
|
||||
INNER JOIN `$sDBTable` AS `removed` ON `updated`.`$sDBField` = `removed`.`$sDBKey`
|
||||
WHERE `removed`.`$sDBKey` IN ($sIdsToRemoveInTargetClass)
|
||||
SQL;
|
||||
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
|
||||
|
||||
return new DeletionPlanItem($aIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sRemoteClass
|
||||
* @param string $sExtKeyAttCode
|
||||
* @param string $sIdsToRemoveInTargetClass
|
||||
*
|
||||
* @return array
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function GetRemoteIdsForExtKey(string $sRemoteClass, string $sExtKeyAttCode, string $sIdsToRemoveInTargetClass): array
|
||||
{
|
||||
if (\utils::IsNullOrEmptyString($sIdsToRemoveInTargetClass)) {
|
||||
return [];
|
||||
}
|
||||
[$sDBTable, $sDBField, $sDBKey] = $this->GetDBInfoForAttcode($sRemoteClass, $sExtKeyAttCode);
|
||||
$sSQL = "SELECT `$sDBKey` FROM `$sDBTable` WHERE `$sDBField` IN ($sIdsToRemoveInTargetClass)";
|
||||
|
||||
return CMDBSource::QueryToCol($sSQL, $sDBKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
*
|
||||
* @return \Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanItem
|
||||
* @throws \CoreException
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function GetInitialClassDeletionPlan(string $sClass): DeletionPlanItem
|
||||
{
|
||||
$sTable = MetaModel::DBGetTable($sClass);
|
||||
$sDBKey = MetaModel::DBGetKey($sClass);
|
||||
$sSQL = "SELECT `$sDBKey` FROM `$sTable`";
|
||||
$aIds = CMDBSource::QueryToCol($sSQL, $sDBKey);
|
||||
|
||||
return new DeletionPlanItem($aIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database table for an attcode
|
||||
*
|
||||
* @param string $sClass
|
||||
* @param string $sExtKeyAttCode
|
||||
*
|
||||
* @return array
|
||||
* @throws \CoreException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetDBInfoForAttcode(string $sClass, string $sExtKeyAttCode): array
|
||||
{
|
||||
$sOriginClass = MetaModel::GetAttributeOrigin($sClass, $sExtKeyAttCode);
|
||||
$sDBTable = MetaModel::DBGetTable($sOriginClass);
|
||||
$oAttDef = MetaModel::GetAttributeDef($sOriginClass, $sExtKeyAttCode);
|
||||
// External key is on a single DB column
|
||||
$sDBField = array_keys($oAttDef->GetSQLColumns())[0];
|
||||
$sDBKey = MetaModel::DBGetKey($sClass);
|
||||
return [$sDBTable, $sDBField, $sDBKey];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
{% if bDeletionNeeded %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% if bDeletionPossible %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% else %}
|
||||
{% UIAlert ForFailure { sTitle: '', sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s } %}{% EndUIAlert %}
|
||||
|
||||
{% UIForm Standard {sId:'launch-setup-form', Action:sLaunchSetupUrl, EncType: 'application/x-www-form-urlencoded'} %}
|
||||
{% for sKey, sValue in aSetupParams %}
|
||||
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
|
||||
{% if bHasDeletionExecution %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% endif %}
|
||||
@@ -1,15 +1,31 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
|
||||
|
||||
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:Analysis:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:Analysis:Subtitle'|dict_s} %}
|
||||
{% if null != DataFeatureRemovalErrorMessage %}
|
||||
<div id="feature_removal_error_msg_div" style="display:block">
|
||||
{% UIAlert ForFailure { sTitle:'DataFeatureRemoval:Failure:Title'|dict_s, sId: 'feature_removal_error_msg', sContent:DataFeatureRemovalErrorMessage } %}
|
||||
{% EndUIAlert %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_compile_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:Compile:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'success', sId: 'ajax_compile_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_compile_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% UIAlert ForInformation { sTitle: '', sId: 'ajax_run_audit_in_progress_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}
|
||||
{{ 'DataFeatureRemoval:RunAudit:InProgress'|dict_s }}
|
||||
{% UISpinner Standard { } %}
|
||||
{% EndUIAlert %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:RunAudit:Success'|dict_s, sId: 'ajax_run_audit_success_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
{% UIAlert ForFailure { sTitle:'error', sId: 'ajax_run_audit_error_msg', AddCSSClasses: ['ibo-is-hidden', 'ibo-is-html-content'] } %}{% EndUIAlert %}
|
||||
|
||||
{% if aAvailableExtensionsCount == 0 %}
|
||||
{% UITitle Neutral { sTitle:'DataFeatureRemoval:NoResult:Title'|dict_s, iLevel:2 } %}{% EndUITitle %}
|
||||
{% else %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
{% for iColumnIndex in 0..iColumnCount-1 %}
|
||||
@@ -25,77 +41,15 @@
|
||||
{% endfor %}
|
||||
{% EndUIMultiColumn %}
|
||||
{% EndUIPanel %}
|
||||
{% else %}
|
||||
{% UIPanel Neutral { sTitle:'DataFeatureRemoval:Result:Title'|dict_s, sSubTitle: '' } %}
|
||||
{% UIMultiColumn Standard {} %}
|
||||
{% for iColumnIndex in 0..iColumnCount-1 %}
|
||||
{% UIColumn Standard {} %}
|
||||
{% for aExtension in aAvailableExtensions[iColumnIndex] %}
|
||||
{% if aExtension['installed'] %}
|
||||
{% UIExtensionDetails Installed { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% else %}
|
||||
{% UIExtensionDetails NotInstalled { sCode : aExtension['code'], sLabel : aExtension['label'], sDescription : aExtension['description'], aMetaData : [aExtension['version'], aExtension['source']], aExtraFlags : aExtension['extra_flags']} %}{% EndUIExtensionDetails %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% EndUIColumn %}
|
||||
{% endfor %}
|
||||
{% EndUIMultiColumn %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
{% if bDeletionNeeded %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:DeletionPlan:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% if bDeletionPossible %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
|
||||
{% for sKey, sClass in aClasses %}
|
||||
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
|
||||
{% endfor %}
|
||||
{% for sCode, sLabel in aAddedExtensions %}
|
||||
{% UIInput ForHidden { sName:"aAddedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
|
||||
{% endfor %}
|
||||
{% for sCode, sLabel in aRemovedExtensions %}
|
||||
{% UIInput ForHidden { sName:"aRemovedExtensions[" ~ sCode ~ "]", sValue:sLabel } %}
|
||||
{% endfor %}
|
||||
{% for sInputName, sValue in aHiddenInputs %}
|
||||
{% UIInput ForHidden { sName:sInputName, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% else %}
|
||||
{% UIAlert ForFailure { sContent: 'DataFeatureRemoval:DeletionPlan:Error:Issues'|dict_s } %}{% EndUIAlert %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% UIAlert ForSuccess { sTitle:'DataFeatureRemoval:CleanupComplete:Title'|dict_s, sContent:'DataFeatureRemoval:CompilComplete'|dict_s, sId:value } %}{% EndUIAlert %}
|
||||
|
||||
{% UIForm Standard {'sId':'launch-setup-form', Action:sLaunchSetupUrl, 'EncType': 'application/x-www-form-urlencoded'} %}
|
||||
{% for sKey, sValue in aSetupParams %}
|
||||
{% UIInput ForHidden { sName:sKey, sValue:sValue } %}
|
||||
{% endfor %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:true} %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
|
||||
{% if bHasDeletionExecution %}
|
||||
{% UIFieldSet Standard {sLegend:'DataFeatureRemoval:Execution:Title'|dict_s} %}
|
||||
{% UIDataTable ForForm { sRef:'aDeletionExecutionSummary', aColumns:aDeletionExecutionSummary.Columns, aData:aDeletionExecutionSummary.Data} %}{% EndUIDataTable %}
|
||||
{% EndUIFieldSet %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if aHiddenInputs.return_button_label != '' %}
|
||||
{% UIButton ForAlternativeSecondaryAction { sLabel:aHiddenInputs.return_button_label, OnClickJsCode: 'window.location.href="' ~ sReturnButtonURL ~ '"'} %}
|
||||
{% else %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
<div id="ajax_run_audit" class="ibo-block"></div>
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
function ajax_run_audit(sTransactionId) {
|
||||
$('#ajax_run_audit_in_progress_msg').removeClass('ibo-is-hidden');
|
||||
$.post(
|
||||
'{{ sAjaxURL|raw }}',
|
||||
{ operation: 'ajax_run_audit', transaction_id: sTransactionId },
|
||||
function (data) {
|
||||
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
|
||||
|
||||
if (data.error_message) {
|
||||
$('#ajax_run_audit_error_msg .ibo-alert--title').html(data.error_message);
|
||||
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
|
||||
} else {
|
||||
$('#ajax_run_audit_success_msg').removeClass('ibo-is-hidden');
|
||||
$('#ajax_run_audit').html(data);
|
||||
}
|
||||
}
|
||||
)
|
||||
.fail(function() {
|
||||
$('#ajax_run_audit_in_progress_msg').addClass('ibo-is-hidden');
|
||||
$('#ajax_run_audit_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:RunAudit:Error'|dict_s }}');
|
||||
$('#ajax_run_audit_error_msg').removeClass('ibo-is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function ajax_compile() {
|
||||
$('#ajax_compile_in_progress_msg').removeClass('ibo-is-hidden');
|
||||
|
||||
$.post(
|
||||
'{{ sAjaxURL|raw }}',
|
||||
{ operation: 'ajax_compile', transaction_id: '{{ sTransactionId }}' },
|
||||
function (data) {
|
||||
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
|
||||
|
||||
if (data.error_message) {
|
||||
$('#ajax_compile_error_msg .ibo-alert--title').html(data.error_message);
|
||||
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
|
||||
} else {
|
||||
$('#ajax_compile_success_msg .ibo-alert--title').html(data.success_message);
|
||||
$('#ajax_compile_success_msg').removeClass('ibo-is-hidden');
|
||||
|
||||
ajax_run_audit(data.transaction_id);
|
||||
}
|
||||
},
|
||||
'json'
|
||||
)
|
||||
.fail(function() {
|
||||
$('#ajax_compile_in_progress_msg').addClass('ibo-is-hidden');
|
||||
$('#ajax_compile_error_msg .ibo-alert--title').html('{{ 'DataFeatureRemoval:Compile:Error'|dict_s }}');
|
||||
$('#ajax_compile_error_msg').removeClass('ibo-is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
ajax_compile();
|
||||
@@ -8,6 +8,8 @@ $baseDir = dirname($vendorDir);
|
||||
return array(
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => $baseDir . '/src/Entity/DataCleanupSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => $baseDir . '/src/Entity/DeletionPlanEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => $baseDir . '/src/Entity/DeletionPlanItem.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
|
||||
@@ -16,6 +18,7 @@ return array(
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => $baseDir . '/src/Service/ObjectService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => $baseDir . '/src/Service/ObjectServiceSummary.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => $baseDir . '/src/Service/StaticDeletionPlan.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => $baseDir . '/src/Service/iObjectService.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
|
||||
public static $classMap = array (
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DataCleanupSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DataCleanupSummaryEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanEntity.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanItem' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanItem.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
|
||||
@@ -31,6 +33,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectService' => __DIR__ . '/../..' . '/src/Service/ObjectService.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\ObjectServiceSummary' => __DIR__ . '/../..' . '/src/Service/ObjectServiceSummary.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\StaticDeletionPlan' => __DIR__ . '/../..' . '/src/Service/StaticDeletionPlan.php',
|
||||
'Combodo\\iTop\\DataFeatureRemoval\\Service\\iObjectService' => __DIR__ . '/../..' . '/src/Service/iObjectService.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
@@ -16,8 +16,8 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'DBTools:Class' => 'Класс',
|
||||
'DBTools:Title' => 'Инструменты обслуживания базы данных',
|
||||
'DBTools:ErrorsFound' => 'Найденные ошибки',
|
||||
'DBTools:Indication' => 'Important: after fixing errors in the database you\'ll have to run the analysis again as new inconsistencies will be generated~~',
|
||||
'DBTools:Disclaimer' => 'DISCLAIMER: BACKUP YOUR DATABASE BEFORE RUNNING THE FIXES~~',
|
||||
'DBTools:Indication' => 'Важно: после исправления ошибок в базе данных нужно будет запустить анализ заново, так как появятся новые несоответствия',
|
||||
'DBTools:Disclaimer' => 'ВНИМАНИЕ: СДЕЛАЙТЕ РЕЗЕРВНУЮ КОПИЮ БАЗЫ ДАННЫХ ПЕРЕД ЗАПУСКОМ ИСПРАВЛЕНИЙ',
|
||||
'DBTools:Error' => 'Ошибка',
|
||||
'DBTools:Count' => 'Количество',
|
||||
'DBTools:SQLquery' => 'SQL-запрос',
|
||||
@@ -28,23 +28,23 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'DBTools:ShowIds' => 'Подробный вид',
|
||||
'DBTools:ShowReport' => 'Отчёт',
|
||||
'DBTools:IntegrityCheck' => 'Проверка целостности',
|
||||
'DBTools:FetchCheck' => 'Fetch Check (long)~~',
|
||||
'DBTools:SelectAnalysisType' => 'Select analysis type~~',
|
||||
'DBTools:FetchCheck' => 'Проверка выборки (долго)',
|
||||
'DBTools:SelectAnalysisType' => 'Выберите тип анализа',
|
||||
'DBTools:Analyze' => 'Анализировать',
|
||||
'DBTools:Details' => 'Показать подробности',
|
||||
'DBTools:ShowAll' => 'Показать все ошибки',
|
||||
'DBTools:Inconsistencies' => 'Несоответствия базы данных',
|
||||
'DBTools:DetailedErrorTitle' => '%2$s error(s) in class %1$s: %3$s~~',
|
||||
'DBTools:DetailedErrorLimit' => 'List limited to %1$s errors~~',
|
||||
'DBTools:DetailedErrorTitle' => 'Ошибок (%2$s) в классе %1$s: %3$s',
|
||||
'DBTools:DetailedErrorLimit' => 'Список ограничен %1$s ошибками',
|
||||
'DBAnalyzer-Integrity-OrphanRecord' => 'Сиротская запись в `%1$s`, она должна иметь свой аналог в таблице `%2$s`',
|
||||
'DBAnalyzer-Integrity-InvalidExtKey' => 'Недопустимый внешний ключ %1$s (столбец: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-MissingExtKey' => 'Отсутствует внешний ключ %1$s (столбец: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-InvalidValue' => 'Недопустимое значение для %1$s (столбец: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-UsersWithoutProfile' => 'Некоторые учетные записи пользователей не имеют профилей',
|
||||
'DBAnalyzer-Integrity-HKInvalid' => 'Broken hierarchical key `%1$s`~~',
|
||||
'DBAnalyzer-Fetch-Count-Error' => 'Fetch count error in `%1$s`, %2$d entries fetched / %3$d counted~~',
|
||||
'DBAnalyzer-Integrity-FinalClass' => 'Field `%2$s`.`%1$s` must have the same value as `%3$s`.`%1$s`~~',
|
||||
'DBAnalyzer-Integrity-RootFinalClass' => 'Field `%2$s`.`%1$s` must contain a valid class~~',
|
||||
'DBAnalyzer-Integrity-HKInvalid' => 'Повреждён иерархический ключ `%1$s`',
|
||||
'DBAnalyzer-Fetch-Count-Error' => 'Ошибка количества выборки в `%1$s`: получено записей %2$d / посчитано %3$d',
|
||||
'DBAnalyzer-Integrity-FinalClass' => 'Поле `%2$s`.`%1$s` должно иметь то же значение, что и `%3$s`.`%1$s`',
|
||||
'DBAnalyzer-Integrity-RootFinalClass' => 'Поле `%2$s`.`%1$s` должно содержать допустимый класс',
|
||||
]);
|
||||
|
||||
// Database Info
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Localized data
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
@@ -21,38 +21,42 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// Database inconsistencies
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
// Dictionary entries go here
|
||||
'Menu:DBToolsMenu' => '数据库工具',
|
||||
'Menu:DBToolsMenu' => '数据库完整性',
|
||||
'DBTools:Class' => '类型',
|
||||
'DBTools:Title' => '数据库维护工具',
|
||||
'DBTools:Title' => '数据库完整性检查',
|
||||
'DBTools:ErrorsFound' => '发现错误',
|
||||
'DBTools:Indication' => '重要: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
|
||||
'DBTools:Indication' => '重要提示: 修复数据库错误后, 可能会出现新的不一致, 您必须重新运行一次分析.',
|
||||
'DBTools:Disclaimer' => '免责申明: 在应用修复之前, 应先备份数据库',
|
||||
'DBTools:Error' => '错误',
|
||||
'DBTools:Count' => '个数',
|
||||
'DBTools:SQLquery' => 'SQL查询',
|
||||
'DBTools:FixitSQLquery' => '修复问题的SQL查询 (指示)',
|
||||
'DBTools:SQLresult' => 'SQL结果',
|
||||
'DBTools:NoError' => '数据库正确',
|
||||
'DBTools:SQLquery' => 'SQL 查询',
|
||||
'DBTools:FixitSQLquery' => '用于修复问题的 SQL 查询(说明)',
|
||||
'DBTools:SQLresult' => 'SQL 结果',
|
||||
'DBTools:NoError' => '数据库 OK',
|
||||
'DBTools:HideIds' => '错误列表',
|
||||
'DBTools:ShowIds' => '详细视图',
|
||||
'DBTools:ShowReport' => '报告',
|
||||
'DBTools:IntegrityCheck' => '完整性检查',
|
||||
'DBTools:FetchCheck' => '提取检查 (耗时长)',
|
||||
'DBTools:SelectAnalysisType' => '请选择分析类型',
|
||||
|
||||
'DBTools:Analyze' => '分析',
|
||||
'DBTools:Details' => '显示详情',
|
||||
'DBTools:ShowAll' => '显示所有错误',
|
||||
|
||||
'DBTools:Inconsistencies' => '数据库不一致',
|
||||
'DBTools:DetailedErrorTitle' => '%2$s个错误在类型%1$s: %3$s',
|
||||
'DBTools:DetailedErrorLimit' => '列表限制为%1$s错误',
|
||||
|
||||
'DBAnalyzer-Integrity-OrphanRecord' => '孤立记录位于 `%1$s`, 其应该有副本位于表 `%2$s`',
|
||||
'DBAnalyzer-Integrity-InvalidExtKey' => '无效的外键%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-MissingExtKey' => '外键丢失%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-InvalidValue' => '无效的值%1$s (列: `%2$s.%3$s`)',
|
||||
'DBAnalyzer-Integrity-UsersWithoutProfile' => '一些用户账号没有角色',
|
||||
'DBAnalyzer-Integrity-UsersWithoutProfile' => '有的账号没有角色',
|
||||
'DBAnalyzer-Integrity-HKInvalid' => '损坏的层级链 `%1$s`',
|
||||
'DBAnalyzer-Fetch-Count-Error' => '读取计数出错于 `%1$s`, %2$d个记录已读取 / %3$d已',
|
||||
'DBAnalyzer-Integrity-FinalClass' => '字段 `%2$s`.`%1$s` 必须是相同的值, 而不是 `%3$s`.`%1$s`',
|
||||
@@ -69,20 +73,25 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
// Lost attachments
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'DBTools:LostAttachments' => '附件缺失',
|
||||
'DBTools:LostAttachments:Disclaimer' => '可以在此搜索数据库中丢失或错放的附件. 这不是数据恢复工具, 其无法恢复已删除的数据.',
|
||||
'DBTools:LostAttachments:Disclaimer' => '可以在此搜索数据库中丢失或错放的附件. 请注意, 这不是数据恢复工具, 无法恢复已删除的数据.',
|
||||
|
||||
'DBTools:LostAttachments:Button:Analyze' => '分析',
|
||||
'DBTools:LostAttachments:Button:Restore' => '还原',
|
||||
'DBTools:LostAttachments:Button:Restore:Confirm' => '此操作无法回退, 请确认是否继续还原.',
|
||||
'DBTools:LostAttachments:Button:Busy' => '请稍候...',
|
||||
|
||||
'DBTools:LostAttachments:Step:Analyze' => '首先, 通过分析数据库来搜索丢失或误挪动的附件.',
|
||||
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults' => '分析结果:',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:None' => '非常好! 所有附件都是正常的.',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Some' => '某些附件 (%1$d) 看起来放错了位置. 请检查下面的列表并选择要挪动的文件.',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:Filename' => '文件名',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:CurrentLocation' => '当前位置',
|
||||
'DBTools:LostAttachments:Step:AnalyzeResults:Item:TargetLocation' => '移动到...',
|
||||
|
||||
'DBTools:LostAttachments:Step:RestoreResults' => '还原结果:',
|
||||
'DBTools:LostAttachments:Step:RestoreResults:Results' => '%1$d/%2$d的附件被还原.',
|
||||
|
||||
'DBTools:LostAttachments:StoredAsInlineImage' => '存储为内嵌图像',
|
||||
'DBTools:LostAttachments:History' => '附件 "%1$s" 已使用数据库工具还原',
|
||||
]);
|
||||
|
||||
@@ -90,6 +90,6 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Class:TriggerOnAttachmentDelete' => 'Déclencheur sur la suppression d\'une pièce jointe',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Déclencheur sur la suppression d\'une pièce jointe d\'un objet',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Ajoute le fichier supprimé dans l\'email',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'Si coché, le fichier supprimé sera automatiquement joint à l\'email quand une action email sera lancée',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Les Triggers sur les objets ne sont pas autorisés sur la classe Attachement. Veuillez utiliser les triggers spécifiques pour cette classe',
|
||||
]);
|
||||
|
||||
@@ -26,12 +26,12 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Attachments:NoAttachment' => 'Нет вложений.',
|
||||
'Attachments:PreviewNotAvailable' => 'Предварительный просмотр не доступен для этого типа вложений.',
|
||||
'Attachments:Error:FileTooLarge' => 'Файл слишком велик для загрузки. %1$s',
|
||||
'Attachments:Error:UploadedFileEmpty' => 'The received file is empty and cannot be attached.
|
||||
Either you have pushed an empty file,
|
||||
or ask your '.ITOP_APPLICATION_SHORT.' administrator if the '.ITOP_APPLICATION_SHORT.' server disk is full.~~',
|
||||
'Attachments:Render:Icons' => 'Display as icons~~',
|
||||
'Attachments:Render:Table' => 'Display as list~~',
|
||||
'UI:Attachments:DropYourFileHint' => 'Drop files anywhere in this area~~',
|
||||
'Attachments:Error:UploadedFileEmpty' => 'Полученный файл пуст и не может быть прикреплён.
|
||||
Либо вы загрузили пустой файл,
|
||||
либо обратитесь к администратору '.ITOP_APPLICATION_SHORT.' — возможно, диск сервера '.ITOP_APPLICATION_SHORT.' переполнен.',
|
||||
'Attachments:Render:Icons' => 'Отображать как иконки',
|
||||
'Attachments:Render:Table' => 'Отображать как список',
|
||||
'UI:Attachments:DropYourFileHint' => 'Перетащите файлы в любое место этой области',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -62,7 +62,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Attachments:File:Uploader' => 'Пользователь',
|
||||
'Attachments:File:Size' => 'Размер',
|
||||
'Attachments:File:MimeType' => 'Тип',
|
||||
'Attachments:File:DownloadsCount' => 'Downloads~~',
|
||||
'Attachments:File:DownloadsCount' => 'Скачиваний',
|
||||
]);
|
||||
//
|
||||
// Class: Attachment
|
||||
@@ -82,15 +82,15 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
//
|
||||
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:TriggerOnAttachmentDownload' => 'Trigger (on object\'s attachment download)~~',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Trigger on object\'s attachment download of [a child class of] the given class~~',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
'Class:TriggerOnAttachmentDownload' => 'Триггер (на скачивание вложения объекта)',
|
||||
'Class:TriggerOnAttachmentDownload+' => 'Триггер на скачивание вложения объекта заданного класса (или его дочернего класса)',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Триггер (на создание вложения объекта)',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Триггер на создание вложения объекта',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Добавлять файл в email',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'Если отмечено, файл будет автоматически прикреплён к письму при срабатывании действия email',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Триггер (на удаление вложения объекта)',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Триггер на удаление вложения объекта',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Добавлять удалённый файл в email',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'Если отмечено, удалённый файл будет автоматически прикреплён к письму при срабатывании действия email',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Триггер на объект не допускается для класса Attachment. Используйте специальный триггер',
|
||||
]);
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Localized data
|
||||
* Copyright (C) 2013-2024 Combodo SAS
|
||||
*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Attachments:TabTitle_Count' => '附件 (%1$d)',
|
||||
'Attachments:EmptyTabTitle' => '附件',
|
||||
@@ -39,10 +47,10 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:Attachment' => '附件',
|
||||
'Class:Attachment+' => '',
|
||||
'Class:Attachment+' => '文件: 链接到单一对象的文本或图片. 它无法被修改,只能被删除. 附件无法在编辑界面之外创建.',
|
||||
'Class:Attachment/Attribute:expire' => '过期',
|
||||
'Class:Attachment/Attribute:expire+' => '',
|
||||
'Class:Attachment/Attribute:temp_id' => '临时编号',
|
||||
'Class:Attachment/Attribute:temp_id' => '临时id',
|
||||
'Class:Attachment/Attribute:temp_id+' => '',
|
||||
'Class:Attachment/Attribute:item_class' => '项目类型',
|
||||
'Class:Attachment/Attribute:item_class+' => '',
|
||||
@@ -69,11 +77,11 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:Attachment/Attribute:creation_date' => '创建日期',
|
||||
'Class:Attachment/Attribute:creation_date+' => '~~',
|
||||
'Class:Attachment/Attribute:user_id' => '用户编号',
|
||||
'Class:Attachment/Attribute:user_id+' => '~~',
|
||||
'Class:Attachment/Attribute:contact_id' => '联系人编号',
|
||||
'Class:Attachment/Attribute:contact_id+' => '~~',
|
||||
'Class:Attachment/Attribute:creation_date+' => '',
|
||||
'Class:Attachment/Attribute:user_id' => '用户id',
|
||||
'Class:Attachment/Attribute:user_id+' => '',
|
||||
'Class:Attachment/Attribute:contact_id' => '联系人id',
|
||||
'Class:Attachment/Attribute:contact_id+' => '',
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -81,15 +89,15 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:TriggerOnAttachmentDownload' => '触发器 (于对象附件下载时)',
|
||||
'Class:TriggerOnAttachmentDownload+' => '触发器于指定类型 [子类型] 对象附件下载时',
|
||||
'Class:TriggerOnAttachmentCreate' => 'Trigger (on object\'s attachment creation)~~',
|
||||
'Class:TriggerOnAttachmentCreate+' => 'Trigger on object\'s attachment creation~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => 'Add file in email~~',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => 'If checked, the file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnAttachmentDelete' => 'Trigger (on object\'s attachment deletion)~~',
|
||||
'Class:TriggerOnAttachmentDelete+' => 'Trigger on object\'s attachment deletion~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => 'Add deleted file in email~~',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => 'If checked, the deleted file will be automatically attached to the email when an email action is triggered~~',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => 'Trigger on object is not allowed on class Attachment. Please use specific trigger~~',
|
||||
'Class:TriggerOnAttachmentDownload' => '触发器 (对象附件被下载时)',
|
||||
'Class:TriggerOnAttachmentDownload+' => '触发器基于指定类型 [或子类型] 对象附件被下载时',
|
||||
'Class:TriggerOnAttachmentCreate' => '触发器 (对象附件被创建时)',
|
||||
'Class:TriggerOnAttachmentCreate+' => '触发器 (对象附件被创建时)',
|
||||
'Class:TriggerOnAttachmentDelete' => '触发器 (对象附件被删除时)',
|
||||
'Class:TriggerOnAttachmentDelete+' => '触发器 (对象附件被删除时)',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email' => '在邮件里添加文件',
|
||||
'Class:TriggerOnAttachmentCreate/Attribute:file_in_email+' => '如果勾选,文件将在触发邮件操作时自动附加到邮件中',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email' => '在邮件里添加已删除的文件',
|
||||
'Class:TriggerOnAttachmentDelete/Attribute:file_in_email+' => '如果勾选,已删除的文件将在触发邮件操作时自动附加到邮件中',
|
||||
'Class:TriggerOnObject:TriggerClassAttachment/ReadOnlyMessage' => '此触发器不允许用于附件. 请使用特定的触发器',
|
||||
]);
|
||||
|
||||
@@ -20,7 +20,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'bkp-mysqldump-ok' => 'Утилита mysqldump найдена: %1$s',
|
||||
'bkp-mysqldump-notfound' => 'Утилиту mysqldump найти не удалось: %1$s - пожалуйста, убедитесь в том, что она установлена, и путь до директории с бинарными файлами добавлен в PATH, либо измените параметр mysql_bindir в файле конфигурации.',
|
||||
'bkp-mysqldump-issue' => 'Утилита mysqldump на может быть запущена (retcode=%1$d) Пожалуйста, убедитесь в том, что она установлена, и путь до директории с бинарными файлами добавлен в PATH, либо измените параметр mysql_bindir в файле конфигурации.',
|
||||
'bkp-missing-dir' => 'The target directory <code>%1$s</code> could not be found~~',
|
||||
'bkp-missing-dir' => 'Целевой каталог <code>%1$s</code> не найден',
|
||||
'bkp-free-disk-space' => '<b>%1$s свободно</b> в <code>%2$s</code>',
|
||||
'bkp-dir-not-writeable' => '%1$s недоступен для записи',
|
||||
'bkp-wrong-format-spec' => 'Неправильный формат шаблона названия файлов резервных копий (%1$s). Будет использован шаблон по умолчанию: %2$s',
|
||||
@@ -38,7 +38,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'bkp-status-backups-manual' => 'Резервное копирование вручную',
|
||||
'bkp-status-backups-none' => 'Резервных копий ещё нет',
|
||||
'bkp-next-backup' => 'Следующее резервное копирование будет выполняться в <b>%1$s</b> (%2$s) в %3$s',
|
||||
'bkp-next-backup-unknown' => 'The next backup is <b>not scheduled</b> yet.~~',
|
||||
'bkp-next-backup-unknown' => 'Следующее резервное копирование пока <b>не запланировано</b>.',
|
||||
'bkp-button-backup-now' => 'Запустить сейчас!',
|
||||
'bkp-button-restore-now' => 'Восстановить!',
|
||||
'bkp-confirm-backup' => 'Пожалуйста, подтвердите, что вы хотите выполнить резервное копирование прямо сейчас.',
|
||||
|
||||
@@ -21,25 +21,28 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
|
||||
'bkp-backup-running' => '备份正在进行, 请稍候...',
|
||||
'bkp-restore-running' => '还原正在进行, 请稍等...',
|
||||
'Menu:BackupStatus' => '定时备份',
|
||||
'bkp-status-title' => '定时备份',
|
||||
|
||||
'Menu:BackupStatus' => '备份',
|
||||
'bkp-status-title' => '备份',
|
||||
'bkp-status-checks' => '设置与检查',
|
||||
'bkp-mysqldump-ok' => '已找到 mysqldump : %1$s',
|
||||
'bkp-mysqldump-notfound' => 'mysqldump找不到: %1$s - 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数mysql_bindir.',
|
||||
'bkp-mysqldump-issue' => 'mysqldump无法运行 (retcode=%1$d): 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数mysql_bindir',
|
||||
'bkp-mysqldump-notfound' => 'mysqldump 找不到: %1$s - 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数 mysql_bindir.',
|
||||
'bkp-mysqldump-issue' => 'mysqldump 无法运行 (retcode=%1$d): 请确认它安装在正确的路径, 或者调整'.ITOP_APPLICATION_SHORT.'配置文件的参数 mysql_bindir',
|
||||
'bkp-missing-dir' => '目标目录<code>%1$s</code>找不到',
|
||||
'bkp-free-disk-space' => '<b>%1$s可用空间</b>位于<code>%2$s</code>',
|
||||
'bkp-dir-not-writeable' => '%1$s没有写入权限',
|
||||
'bkp-wrong-format-spec' => '当前文件名格式错误 (%1$s). 默认格式应该是: %2$s',
|
||||
'bkp-name-sample' => '备份文件将以数据库名, 日期和时间进行命名. 例如: %1$s',
|
||||
'bkp-week-days' => '在每个<b>%1$s的%2$s</b>进行备份',
|
||||
'bkp-retention' => '最多<b>%1$d份备份文件</b>在目标目录.',
|
||||
'bkp-retention' => '在目标目录中最多保留<b>%1$d份备份文件</b>.',
|
||||
'bkp-next-to-delete' => '当下一次备份时将被删除 (参阅设置 "retention_count")',
|
||||
'bkp-table-file' => '文件',
|
||||
'bkp-table-file+' => '只有扩展名是.zip的文件才被认为是备份文件',
|
||||
'bkp-table-file+' => '只有扩展名是.zip的文件才被认为是有效的备份文件',
|
||||
'bkp-table-size' => '大小',
|
||||
'bkp-table-size+' => '',
|
||||
'bkp-table-actions' => '操作',
|
||||
@@ -47,7 +50,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'bkp-status-backups-auto' => '定时备份',
|
||||
'bkp-status-backups-manual' => '手动备份',
|
||||
'bkp-status-backups-none' => '尚未开始备份',
|
||||
'bkp-next-backup' => '下一次备份将发生在<b>%1$s</b> (%2$s) 的%3$s',
|
||||
'bkp-next-backup' => '下一次备份即将发生在<b>%1$s</b> (%2$s) 的%3$s',
|
||||
'bkp-next-backup-unknown' => '下一次备份<b>尚未被计划</b>.',
|
||||
'bkp-button-backup-now' => '立即备份!',
|
||||
'bkp-button-restore-now' => '还原!',
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSet;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Spinner\SpinnerUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
|
||||
@@ -410,8 +409,11 @@ JS
|
||||
|
||||
$sEnvironment = addslashes(utils::GetCurrentEnvironment());
|
||||
|
||||
$oModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup);
|
||||
$sModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oModalSpinner);
|
||||
$oBackupModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitBackup);
|
||||
$sBackupModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oBackupModalSpinner);
|
||||
|
||||
$oRestoreModalSpinner = SpinnerUIBlockFactory::MakeMedium(null, $sPleaseWaitRestore);
|
||||
$sRestoreModalSpinnerHtml = BlockRenderer::RenderBlockTemplates($oRestoreModalSpinner);
|
||||
|
||||
$oP->add_script(
|
||||
<<<JS
|
||||
@@ -424,7 +426,7 @@ function LaunchBackupNow()
|
||||
{
|
||||
const oModal = CombodoModal.OpenModal({
|
||||
title: '$sBackUpNow',
|
||||
content: `$sModalSpinnerHtml`
|
||||
content: `$sBackupModalSpinnerHtml`
|
||||
});
|
||||
|
||||
var oParams = {};
|
||||
@@ -450,10 +452,10 @@ function LaunchRestoreNow(sBackupFile, sConfirmationMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const oModal = CombodoModal.OpenModal({
|
||||
title: '$sRestore',
|
||||
content: '<i class="ajax-spin fas fa-sync-alt fa-spin"></i> $sPleaseWaitRestore'
|
||||
content: `$sRestoreModalSpinnerHtml`
|
||||
});
|
||||
|
||||
$('#backup_success').addClass('ibo-is-hidden');
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:lnkFunctionalCIToProviderContract' => 'Связь Функциональная КЕ/Договор с поставщиком',
|
||||
'Class:lnkFunctionalCIToProviderContract+' => '',
|
||||
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s~~',
|
||||
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id' => 'Договор с поставщиком',
|
||||
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id+' => '',
|
||||
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_name' => 'Договор с поставщиком',
|
||||
@@ -32,7 +32,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:lnkFunctionalCIToService' => 'Связь Функциональная КЕ/Услуга',
|
||||
'Class:lnkFunctionalCIToService+' => '',
|
||||
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s~~',
|
||||
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_id' => 'Услуга',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_id+' => '',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_name' => 'Услуга',
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @author Benjamin Planque <benjamin.planque@combodo.com>
|
||||
* @author Benjamin Planque <benjamin.planque@combodo.com>
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Note: The classes have been grouped by categories: bizmodel
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -31,8 +32,9 @@
|
||||
//
|
||||
// Class: lnkFunctionalCIToProviderContract
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:lnkFunctionalCIToProviderContract' => '关联功能配置项/供应商合同',
|
||||
'Class:lnkFunctionalCIToProviderContract' => '链接 功能配置项/供应商合同',
|
||||
'Class:lnkFunctionalCIToProviderContract+' => '',
|
||||
'Class:lnkFunctionalCIToProviderContract/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToProviderContract/Attribute:providercontract_id' => '供应商合同',
|
||||
@@ -50,9 +52,9 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:lnkFunctionalCIToService' => '关联 功能配置项/服务',
|
||||
'Class:lnkFunctionalCIToService+' => '',
|
||||
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s~~',
|
||||
'Class:lnkFunctionalCIToService' => '链接 功能配置项/服务',
|
||||
'Class:lnkFunctionalCIToService+' => 'This link models the functional CIs that are required to deliver a Service and whose malfunction would affect the quality of the Service.~~',
|
||||
'Class:lnkFunctionalCIToService/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_id' => '服务',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_id+' => '',
|
||||
'Class:lnkFunctionalCIToService/Attribute:service_name' => '服务名称',
|
||||
@@ -80,7 +82,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:Document/Attribute:contracts_list' => '合同',
|
||||
'Class:Document/Attribute:contracts_list+' => '此文档关联的所有合同',
|
||||
'Class:Document/Attribute:contracts_list+' => '此文档相关的所有合同',
|
||||
'Class:Document/Attribute:services_list' => '服务',
|
||||
'Class:Document/Attribute:services_list+' => '此文档关联的所有服务',
|
||||
'Class:Document/Attribute:services_list+' => '此文档相关的所有服务',
|
||||
]);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Dict::Add('RU RU', 'Russian', 'Русский', [
|
||||
'Class:lnkFunctionalCIToTicket' => 'Связь Функциональная КЕ/Тикет',
|
||||
'Class:lnkFunctionalCIToTicket+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s~~',
|
||||
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id' => 'Тикет',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref' => 'Тикет',
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license https://opensource.org/licenses/AGPL-3.0
|
||||
* @author Benjamin Planque <benjamin.planque@combodo.com>
|
||||
*/
|
||||
* @author Benjamin Planque <benjamin.planque@combodo.com>
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Note: The classes have been grouped by categories: bizmodel
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -28,19 +29,21 @@
|
||||
// Classes in 'bizmodel'
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
|
||||
//
|
||||
// Class: lnkFunctionalCIToTicket
|
||||
//
|
||||
|
||||
Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:lnkFunctionalCIToTicket' => '关联 功能配置项/工单',
|
||||
'Class:lnkFunctionalCIToTicket+' => '',
|
||||
'Class:lnkFunctionalCIToTicket' => '链接 功能配置项/工单',
|
||||
'Class:lnkFunctionalCIToTicket+' => 'This link models the functional CIs that are concerned by a Ticket (such as a User Request, an Incident, a Change, ...)~~',
|
||||
'Class:lnkFunctionalCIToTicket/Name' => '%1$s / %2$s',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id' => '工单',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_id+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref' => '工单编号',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_ref+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title' => '工单标题',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title+' => '~~',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:ticket_title+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id' => '配置项',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_id+' => '',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:functionalci_name' => '配置项名称',
|
||||
@@ -50,7 +53,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:impact_code' => '影响',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:manual' => '手动添加',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:computed' => '自动添加',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => '不通知',
|
||||
'Class:lnkFunctionalCIToTicket/Attribute:impact_code/Value:not_impacted' => '未被影响',
|
||||
]);
|
||||
|
||||
//
|
||||
|
||||
@@ -21,11 +21,11 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
|
||||
'Menu:SearchChanges' => 'Hledat změny',
|
||||
'Menu:SearchChanges+' => 'Hledat změnové tikety',
|
||||
'Menu:Change:Shortcuts' => 'Odkazy',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Změny čekající na přijetí',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Změny čekající na schválení',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Otevřené změny',
|
||||
'Menu:Changes+' => 'Všechny otevřené změny',
|
||||
'Menu:MyChanges' => 'Změny přidělené mně',
|
||||
|
||||
@@ -20,11 +20,11 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
|
||||
'Menu:SearchChanges' => 'Søg efter Changes',
|
||||
'Menu:SearchChanges+' => 'Søg efter Change Tickets',
|
||||
'Menu:Change:Shortcuts' => 'Genveje',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Changes, som afventer accept',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Changes, som afventer godkendelse',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Offene Changes',
|
||||
'Menu:Changes+' => 'Alle åbne Changes',
|
||||
'Menu:MyChanges' => 'Mine Changes',
|
||||
|
||||
@@ -19,8 +19,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
|
||||
'Menu:NewChange+' => 'Einen neuen Change erstellen',
|
||||
'Menu:SearchChanges' => 'Suche nach Changes',
|
||||
'Menu:SearchChanges+' => 'Unter den bestehenden Changes suchen',
|
||||
'Menu:Change:Shortcuts' => 'Shortcuts',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts' => 'Changes~~',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Changes, die auf Annahme warten',
|
||||
'Menu:WaitingAcceptance+' => 'Changes, die auf Annahme warten',
|
||||
'Menu:WaitingApproval' => 'Changes, die auf Genehmigung warten',
|
||||
|
||||
@@ -30,16 +30,16 @@ Dict::Add('EN US', 'English', 'English', [
|
||||
'Menu:NewChange+' => 'Create a new change ticket',
|
||||
'Menu:SearchChanges' => 'Search for changes',
|
||||
'Menu:SearchChanges+' => 'Search for change tickets',
|
||||
'Menu:Change:Shortcuts' => 'Shortcuts',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts' => 'Changes',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes',
|
||||
'Menu:WaitingAcceptance' => 'Changes awaiting acceptance',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Changes awaiting approval',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status',
|
||||
'Menu:Changes' => 'Open changes',
|
||||
'Menu:Changes+' => 'All open changes',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status',
|
||||
'Menu:MyChanges' => 'Changes assigned to me',
|
||||
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)',
|
||||
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changes by category for the last 7 days',
|
||||
'UI-ChangeManagementOverview-Last-7-days' => 'Number of changes for the last 7 days',
|
||||
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes by domain for the last 7 days',
|
||||
|
||||
@@ -30,16 +30,16 @@ Dict::Add('EN GB', 'British English', 'British English', [
|
||||
'Menu:NewChange+' => 'Create a new change ticket',
|
||||
'Menu:SearchChanges' => 'Search for changes',
|
||||
'Menu:SearchChanges+' => 'Search for change tickets',
|
||||
'Menu:Change:Shortcuts' => 'Shortcuts',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts' => 'Changes~~',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Changes awaiting acceptance',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Changes awaiting approval',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Open changes',
|
||||
'Menu:Changes+' => 'All open changes',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status',
|
||||
'Menu:MyChanges' => 'Changes assigned to me',
|
||||
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)',
|
||||
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changes by category for the last 7 days',
|
||||
'UI-ChangeManagementOverview-Last-7-days' => 'Number of changes for the last 7 days',
|
||||
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changes by domain for the last 7 days',
|
||||
|
||||
@@ -18,16 +18,16 @@ Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Menu:NewChange+' => 'Créer un nouveau ticket de changement',
|
||||
'Menu:SearchChanges' => 'Rechercher des changements',
|
||||
'Menu:SearchChanges+' => 'Rechercher parmi les tickets de changement',
|
||||
'Menu:Change:Shortcuts' => 'Raccourcis',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:WaitingAcceptance' => 'Changements en attente d\'acceptance',
|
||||
'Menu:WaitingAcceptance+' => 'Changements en attente d\'acceptance',
|
||||
'Menu:WaitingApproval' => 'Changements en attente d\'approbation',
|
||||
'Menu:WaitingApproval+' => 'Changements en attente d\'approbation',
|
||||
'Menu:Changes' => 'Changements ouverts',
|
||||
'Menu:Changes+' => 'Tickets de changement ouverts',
|
||||
'Menu:MyChanges' => 'Mes changements',
|
||||
'Menu:MyChanges+' => 'Tickets de changement qui me sont assignés',
|
||||
'Menu:Change:Shortcuts' => 'Changements',
|
||||
'Menu:Change:Shortcuts+' => 'Raccourcis vers différents groupes de changements',
|
||||
'Menu:WaitingAcceptance' => 'En attente de validation',
|
||||
'Menu:WaitingAcceptance+' => 'Changements à l\'état nouveau, en attente d\'acceptation',
|
||||
'Menu:WaitingApproval' => 'En attente d\'approbation',
|
||||
'Menu:WaitingApproval+' => 'Changements dans l\'état programmé, en attente d\'approbation',
|
||||
'Menu:Changes' => 'En cours',
|
||||
'Menu:Changes+' => 'Tickets de changement qui ne sont pas dans l\'état fermé',
|
||||
'Menu:MyChanges' => 'Qui me sont assignés',
|
||||
'Menu:MyChanges+' => 'Changements ni fermés, ni résolus, dont je suis l\'agent assigné',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Changements par catégorie',
|
||||
'UI-ChangeManagementOverview-Last-7-days' => 'Changements par jour',
|
||||
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Changements par domaine',
|
||||
|
||||
@@ -19,11 +19,11 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
|
||||
'Menu:SearchChanges' => 'Cerca per cambi',
|
||||
'Menu:SearchChanges+' => 'Cerca i cambi per tickets',
|
||||
'Menu:Change:Shortcuts' => 'Scorciatoie',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Modifiche in attesa di accettazione',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Modifiche in attesa di approvazione',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Modifiche aperte',
|
||||
'Menu:Changes+' => 'Tutte le Modifiche aperte',
|
||||
'Menu:MyChanges' => 'Modifiche assegnate a me',
|
||||
|
||||
@@ -19,13 +19,13 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
|
||||
'Menu:SearchChanges' => '変更検索',
|
||||
'Menu:SearchChanges+' => '変更チケット検索',
|
||||
'Menu:Change:Shortcuts' => 'ショートカット',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => '受理待ちの変更',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => '承認待ちの変更',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'オープン状態の変更',
|
||||
'Menu:Changes+' => '',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status~~',
|
||||
'Menu:MyChanges' => '担当している変更',
|
||||
'Menu:MyChanges+' => '担当している変更(エージェントとして)',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => '最近7日間のカテゴリ別の変更',
|
||||
|
||||
@@ -21,11 +21,11 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
|
||||
'Menu:SearchChanges' => 'Zoek naar changes',
|
||||
'Menu:SearchChanges+' => 'Zoek naar changes',
|
||||
'Menu:Change:Shortcuts' => 'Snelkoppelingen',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Changes die acceptatie vereisen',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Changes die goedkeuring vereisen',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Open changes',
|
||||
'Menu:Changes+' => 'Alle open changes',
|
||||
'Menu:MyChanges' => 'Changes toegewezen aan mij',
|
||||
|
||||
@@ -19,11 +19,11 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
|
||||
'Menu:SearchChanges' => 'Szukaj zmian',
|
||||
'Menu:SearchChanges+' => 'Szukaj zgłoszeń zmian',
|
||||
'Menu:Change:Shortcuts' => 'Skróty',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Zmiany do akceptacji',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Zmiany do zatwierdzenia',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Otwarte zmiany',
|
||||
'Menu:Changes+' => 'Wszystkie otwarte zmiany',
|
||||
'Menu:MyChanges' => 'Zmiany przypisane do mnie',
|
||||
|
||||
@@ -19,13 +19,13 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
|
||||
'Menu:SearchChanges' => 'Pesquisar por mudanças',
|
||||
'Menu:SearchChanges+' => 'Pesquisar por solicitações de mudança',
|
||||
'Menu:Change:Shortcuts' => 'Atalhos',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Mudanças aguardando aceitação',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Mudanças aguardando aprovação',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Mudanças abertas',
|
||||
'Menu:Changes+' => '',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status~~',
|
||||
'Menu:MyChanges' => 'Mudanças atribuídas a mim',
|
||||
'Menu:MyChanges+' => 'Mudanças atribuídas a mim (como Agente)',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Mudanças por categoria nos últimos 7 dias',
|
||||
|
||||
@@ -23,11 +23,11 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
|
||||
'Menu:WaitingAcceptance' => 'Zmeny očakávajúce prijatie',
|
||||
'Menu:WaitingAcceptance+' => '~~',
|
||||
'Menu:WaitingApproval' => 'Zmeny očakávajúce schválenie',
|
||||
'Menu:WaitingApproval+' => '~~',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Otvorené zmeny',
|
||||
'Menu:Changes+' => 'All open changes~~',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status~~',
|
||||
'Menu:MyChanges' => 'Zmeny pridelené mne',
|
||||
'Menu:MyChanges+' => 'Changes assigned to me (as Agent)~~',
|
||||
'Menu:MyChanges+' => 'Non-closed changes assigned to me (as an Agent)~~',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Zmeny podľa kategórie za posledných 7 dní',
|
||||
'UI-ChangeManagementOverview-Last-7-days' => 'Počet zmien za posledných 7 dní',
|
||||
'UI-ChangeManagementOverview-ChangeByDomain-last-7-days' => 'Zmeny podľa domény za posledných 7 dní',
|
||||
|
||||
@@ -20,13 +20,13 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
|
||||
'Menu:SearchChanges' => 'Değişiklik ara',
|
||||
'Menu:SearchChanges+' => 'Değişiklik isteği ara',
|
||||
'Menu:Change:Shortcuts' => 'Kısayollar',
|
||||
'Menu:Change:Shortcuts+' => '',
|
||||
'Menu:Change:Shortcuts+' => 'Shortcuts to predefined sets of Changes~~',
|
||||
'Menu:WaitingAcceptance' => 'Kabul bekleyen değişiklik talepleri',
|
||||
'Menu:WaitingAcceptance+' => '',
|
||||
'Menu:WaitingApproval' => 'Onay bekleyen değişiklik talepleri',
|
||||
'Menu:WaitingApproval+' => '',
|
||||
'Menu:WaitingApproval+' => 'Changes in planned status~~',
|
||||
'Menu:Changes' => 'Açık değişiklikler',
|
||||
'Menu:Changes+' => '',
|
||||
'Menu:Changes+' => 'All changes which are not in closed status~~',
|
||||
'Menu:MyChanges' => 'Bana atanan değişiklik istekleri',
|
||||
'Menu:MyChanges+' => 'Bana atanan değişiklik istekleri',
|
||||
'UI-ChangeManagementOverview-ChangeByCategory-last-7-days' => 'Son 7 gün için kategoriye göre değişiklikler',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user